diff --git a/README.md b/README.md index f88c086..8e0af69 100644 --- a/README.md +++ b/README.md @@ -1 +1,86 @@ -# sluggers \ No newline at end of file +# sluggers + +A construction-based 2D platformer originally built in GameMaker. +The `copilot/rewrite-project-in-godot` branch contains a full rewrite of the +project in **Godot 4**. + +--- + +## Original GameMaker project + +The root of this repository contains the GameMaker project files (`.yyp`, +`objects/`, `scripts/`, `rooms/`, `sprites/`, etc.). These remain as a +reference during the Godot rewrite. + +--- + +## Godot rewrite (`godot/`) + +All Godot 4 source lives in the [`godot/`](godot/) subdirectory. + +### Project structure + +``` +godot/ +├── project.godot # Godot 4 project configuration +├── autoloads/ +│ ├── GameState.gd # Global singleton – timer, diamonds, creations, pause +│ └── InputManager.gd # Runtime key-rebinding helper +├── scenes/ +│ ├── player/ +│ │ ├── Player.gd # State-machine player (GROUND / AIR / WALL) +│ │ └── Player.tscn +│ ├── objects/ +│ │ ├── Solid.tscn # Static terrain block +│ │ ├── SolidCreation.* # Player-placed solid block (Z key) +│ │ ├── BouncyCreation.* # Player-placed bouncy block (X key) +│ │ ├── Diamond.* # Collectible gem +│ │ ├── MovingBlock.* # Horizontally-shuttling platform +│ │ ├── Enemy.* # Damages/knocks back player; can be stomped +│ │ ├── DeathArea.* # Kill-zone (pits, spikes) +│ │ └── RoomWarp.* # Teleport trigger between rooms +│ ├── ui/ +│ │ ├── HUD.* # Timer / diamond / creations display +│ │ └── PauseMenu.* # Multi-page pause menu (audio/difficulty/graphics/controls) +│ ├── camera/ +│ │ └── GameCamera.* # Lerp-follow camera +│ └── rooms/ +│ ├── Room0.tscn # Level 1 +│ ├── Room1.tscn # Level 2 +│ ├── Room2.tscn # Level 3 +│ ├── Room3.tscn # Level 4 +│ └── Room4.tscn # Level 5 +└── assets/ + └── README.md # Sprite / audio asset list (to be imported) +``` + +### How to open + +1. Install [Godot 4.3+](https://godotengine.org/download) +2. Open Godot → **Import** → select `godot/project.godot` +3. Press **F5** to run from Room 0 + +### Default controls + +| Action | Key | +|---------------------|--------------| +| Move left | ← Left arrow | +| Move right | → Right arrow | +| Jump | ↑ Up arrow | +| Place solid block | Z | +| Place bouncy block | X | +| Pause | Escape | +| Restart level | R | + +Controls can be rebound from the **Pause → Settings → Controls** menu. + +### Game mechanics + +| Mechanic | Description | +|---|---| +| Ground / Air / Wall states | Full state-machine movement with friction, variable jump height, wall-slide, and wall-jump | +| Block creation | Limited budget of placeable blocks per level (solid or bouncy) | +| Diamonds | Collectibles that accumulate across the run | +| Moving platforms | Horizontal platforms that carry the player | +| Room warps | Teleporters that transition between levels | +| Pause menu | Audio, difficulty, graphics, and rebindable-controls settings | diff --git a/godot/.gitignore b/godot/.gitignore new file mode 100644 index 0000000..f9a1678 --- /dev/null +++ b/godot/.gitignore @@ -0,0 +1,14 @@ +# Godot 4 editor and cache directories +.godot/ + +# Export templates and builds +*.exe +*.x86_64 +*.x86_32 +*.arm64 +*.app +*.apk +*.aab +*.html +*.pck +*.zip diff --git a/godot/assets/README.md b/godot/assets/README.md new file mode 100644 index 0000000..f9a4cac --- /dev/null +++ b/godot/assets/README.md @@ -0,0 +1,31 @@ +# Sluggers – Assets Placeholder + +This directory is reserved for Sluggers' game assets in the Godot rewrite. + +## Expected contents + +| Subdirectory | Contents | +|----------------|--------------------------------------------------------| +| `sprites/` | Player, enemy, block, diamond, background sprites | +| `sounds/` | Sound-effect audio files (`.ogg` or `.wav`) | +| `music/` | Background music tracks (`.ogg`) | +| `fonts/` | Bitmap / pixel fonts | + +## Sprite list (to be ported from the GameMaker project) + +- `s_player` – idle standing sprite +- `s_player_right` – walking animation +- `s_player_jump` – airborne sprite +- `s_player_slide` – wall-slide sprite +- `s_player_attack` – attack animation +- `s_enemy` – enemy sprite +- `s_solid` – static terrain tile +- `s_diamond` – collectible gem sprite +- `s_hitbox` – debug hitbox overlay +- `s_room_warp` – warp zone indicator +- `s_creation_block` – player-placed solid block +- `s_bg` / `s_bg_sky` / `s_bg_clouds_behind` / `s_bg_clouds_front` – parallax layers +- `s_fg` / `s_mg` / `s_mg_city` / `s_mg_silhoette` – foreground / midground layers + +Some scenes may still use simple `ColorRect` placeholder visuals where sprites +have not yet been imported or wired up. diff --git a/godot/assets/sprites/s_bg.png b/godot/assets/sprites/s_bg.png new file mode 100644 index 0000000..ceddea2 Binary files /dev/null and b/godot/assets/sprites/s_bg.png differ diff --git a/godot/assets/sprites/s_bg_clouds_behind.png b/godot/assets/sprites/s_bg_clouds_behind.png new file mode 100644 index 0000000..20d3763 Binary files /dev/null and b/godot/assets/sprites/s_bg_clouds_behind.png differ diff --git a/godot/assets/sprites/s_bg_clouds_front.png b/godot/assets/sprites/s_bg_clouds_front.png new file mode 100644 index 0000000..142d413 Binary files /dev/null and b/godot/assets/sprites/s_bg_clouds_front.png differ diff --git a/godot/assets/sprites/s_bg_sky.png b/godot/assets/sprites/s_bg_sky.png new file mode 100644 index 0000000..3247859 Binary files /dev/null and b/godot/assets/sprites/s_bg_sky.png differ diff --git a/godot/assets/sprites/s_creation_block.png b/godot/assets/sprites/s_creation_block.png new file mode 100644 index 0000000..1a51e6e Binary files /dev/null and b/godot/assets/sprites/s_creation_block.png differ diff --git a/godot/assets/sprites/s_diamond_0.png b/godot/assets/sprites/s_diamond_0.png new file mode 100644 index 0000000..7bd1290 Binary files /dev/null and b/godot/assets/sprites/s_diamond_0.png differ diff --git a/godot/assets/sprites/s_diamond_1.png b/godot/assets/sprites/s_diamond_1.png new file mode 100644 index 0000000..4fe6b9d Binary files /dev/null and b/godot/assets/sprites/s_diamond_1.png differ diff --git a/godot/assets/sprites/s_diamond_2.png b/godot/assets/sprites/s_diamond_2.png new file mode 100644 index 0000000..7bd1290 Binary files /dev/null and b/godot/assets/sprites/s_diamond_2.png differ diff --git a/godot/assets/sprites/s_diamond_3.png b/godot/assets/sprites/s_diamond_3.png new file mode 100644 index 0000000..4fe6b9d Binary files /dev/null and b/godot/assets/sprites/s_diamond_3.png differ diff --git a/godot/assets/sprites/s_diamond_4.png b/godot/assets/sprites/s_diamond_4.png new file mode 100644 index 0000000..7bd1290 Binary files /dev/null and b/godot/assets/sprites/s_diamond_4.png differ diff --git a/godot/assets/sprites/s_diamond_5.png b/godot/assets/sprites/s_diamond_5.png new file mode 100644 index 0000000..2b1c9ae Binary files /dev/null and b/godot/assets/sprites/s_diamond_5.png differ diff --git a/godot/assets/sprites/s_diamond_6.png b/godot/assets/sprites/s_diamond_6.png new file mode 100644 index 0000000..f60dbf2 Binary files /dev/null and b/godot/assets/sprites/s_diamond_6.png differ diff --git a/godot/assets/sprites/s_diamond_7.png b/godot/assets/sprites/s_diamond_7.png new file mode 100644 index 0000000..ff3d4c1 Binary files /dev/null and b/godot/assets/sprites/s_diamond_7.png differ diff --git a/godot/assets/sprites/s_diamond_8.png b/godot/assets/sprites/s_diamond_8.png new file mode 100644 index 0000000..bcd26d5 Binary files /dev/null and b/godot/assets/sprites/s_diamond_8.png differ diff --git a/godot/assets/sprites/s_diamond_9.png b/godot/assets/sprites/s_diamond_9.png new file mode 100644 index 0000000..4fe6b9d Binary files /dev/null and b/godot/assets/sprites/s_diamond_9.png differ diff --git a/godot/assets/sprites/s_enemy_0.png b/godot/assets/sprites/s_enemy_0.png new file mode 100644 index 0000000..fa64aaf Binary files /dev/null and b/godot/assets/sprites/s_enemy_0.png differ diff --git a/godot/assets/sprites/s_enemy_1.png b/godot/assets/sprites/s_enemy_1.png new file mode 100644 index 0000000..33eedd6 Binary files /dev/null and b/godot/assets/sprites/s_enemy_1.png differ diff --git a/godot/assets/sprites/s_fg.png b/godot/assets/sprites/s_fg.png new file mode 100644 index 0000000..7c0bcb3 Binary files /dev/null and b/godot/assets/sprites/s_fg.png differ diff --git a/godot/assets/sprites/s_mg.png b/godot/assets/sprites/s_mg.png new file mode 100644 index 0000000..8a5ee9f Binary files /dev/null and b/godot/assets/sprites/s_mg.png differ diff --git a/godot/assets/sprites/s_mg_city.png b/godot/assets/sprites/s_mg_city.png new file mode 100644 index 0000000..f4c22cc Binary files /dev/null and b/godot/assets/sprites/s_mg_city.png differ diff --git a/godot/assets/sprites/s_mg_silhoette.png b/godot/assets/sprites/s_mg_silhoette.png new file mode 100644 index 0000000..1ed7883 Binary files /dev/null and b/godot/assets/sprites/s_mg_silhoette.png differ diff --git a/godot/assets/sprites/s_player_idle_0.png b/godot/assets/sprites/s_player_idle_0.png new file mode 100644 index 0000000..78df49e Binary files /dev/null and b/godot/assets/sprites/s_player_idle_0.png differ diff --git a/godot/assets/sprites/s_player_idle_1.png b/godot/assets/sprites/s_player_idle_1.png new file mode 100644 index 0000000..577f0e7 Binary files /dev/null and b/godot/assets/sprites/s_player_idle_1.png differ diff --git a/godot/assets/sprites/s_player_idle_2.png b/godot/assets/sprites/s_player_idle_2.png new file mode 100644 index 0000000..406511f Binary files /dev/null and b/godot/assets/sprites/s_player_idle_2.png differ diff --git a/godot/assets/sprites/s_player_jump_0.png b/godot/assets/sprites/s_player_jump_0.png new file mode 100644 index 0000000..5ad7227 Binary files /dev/null and b/godot/assets/sprites/s_player_jump_0.png differ diff --git a/godot/assets/sprites/s_player_jump_1.png b/godot/assets/sprites/s_player_jump_1.png new file mode 100644 index 0000000..d2798a9 Binary files /dev/null and b/godot/assets/sprites/s_player_jump_1.png differ diff --git a/godot/assets/sprites/s_player_slide.png b/godot/assets/sprites/s_player_slide.png new file mode 100644 index 0000000..da0da18 Binary files /dev/null and b/godot/assets/sprites/s_player_slide.png differ diff --git a/godot/assets/sprites/s_player_walk_0.png b/godot/assets/sprites/s_player_walk_0.png new file mode 100644 index 0000000..3c754d0 Binary files /dev/null and b/godot/assets/sprites/s_player_walk_0.png differ diff --git a/godot/assets/sprites/s_player_walk_1.png b/godot/assets/sprites/s_player_walk_1.png new file mode 100644 index 0000000..1652e21 Binary files /dev/null and b/godot/assets/sprites/s_player_walk_1.png differ diff --git a/godot/assets/sprites/s_player_walk_2.png b/godot/assets/sprites/s_player_walk_2.png new file mode 100644 index 0000000..3e8ea9d Binary files /dev/null and b/godot/assets/sprites/s_player_walk_2.png differ diff --git a/godot/assets/sprites/s_player_walk_3.png b/godot/assets/sprites/s_player_walk_3.png new file mode 100644 index 0000000..fa6bf35 Binary files /dev/null and b/godot/assets/sprites/s_player_walk_3.png differ diff --git a/godot/assets/sprites/s_player_walk_4.png b/godot/assets/sprites/s_player_walk_4.png new file mode 100644 index 0000000..bf9e445 Binary files /dev/null and b/godot/assets/sprites/s_player_walk_4.png differ diff --git a/godot/assets/sprites/s_player_walk_5.png b/godot/assets/sprites/s_player_walk_5.png new file mode 100644 index 0000000..d42b684 Binary files /dev/null and b/godot/assets/sprites/s_player_walk_5.png differ diff --git a/godot/assets/sprites/s_player_walk_6.png b/godot/assets/sprites/s_player_walk_6.png new file mode 100644 index 0000000..5ebe7e7 Binary files /dev/null and b/godot/assets/sprites/s_player_walk_6.png differ diff --git a/godot/assets/sprites/s_room_warp.png b/godot/assets/sprites/s_room_warp.png new file mode 100644 index 0000000..fe60184 Binary files /dev/null and b/godot/assets/sprites/s_room_warp.png differ diff --git a/godot/assets/sprites/s_solid.png b/godot/assets/sprites/s_solid.png new file mode 100644 index 0000000..e8ae10d Binary files /dev/null and b/godot/assets/sprites/s_solid.png differ diff --git a/godot/autoloads/GameState.gd b/godot/autoloads/GameState.gd new file mode 100644 index 0000000..339f419 --- /dev/null +++ b/godot/autoloads/GameState.gd @@ -0,0 +1,112 @@ +extends Node + +## Sluggers – GameState (Autoload) +## +## Global singleton that holds all persistent game data: +## pause state, elapsed timer, diamond counter, player-creation resources, +## and stamina. Emits signals so the HUD and other nodes can react reactively +## without polling. + +signal paused +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 + +# ── Timer ────────────────────────────────────────────────────────────────── +var minutes: int = 0 +var seconds: float = 0.0 +var _last_timer_minute: int = -1 +var _last_timer_second: int = -1 + +# ── Collectibles ─────────────────────────────────────────────────────────── +var diamonds: int = 0: + set(value): + field = max(value, 0) + diamonds_changed.emit(field) + +# ── Block-creation resources ─────────────────────────────────────────────── +var creations_allowed: int = 5 +var creations_remaining: int = 5: + set(value): + field = clampi(value, 0, creations_allowed) + creations_changed.emit(field) + +# ── Stamina / health ─────────────────────────────────────────────────────── +var player_stamina: int = 4: + set(value): + field = clampi(value, 0, max_player_stamina) + health_changed.emit(field, max_player_stamina) +var max_player_stamina: int = 4 + +# ── Difficulty (0 = Easy, 1 = Medium, 2 = Extreme) ──────────────────────── +var enemy_difficulty: int = 1 + +# ── Audio volumes (linear 0–1) ───────────────────────────────────────────── +var master_volume: float = 1.0 +var sound_volume: float = 1.0 +var music_volume: float = 1.0 + + +func _process(delta: float) -> void: + if is_paused: + return + seconds += delta + if seconds >= 60.0: + seconds -= 60.0 + minutes += 1 + var displayed_second: int = int(seconds) + if minutes != _last_timer_minute or displayed_second != _last_timer_second: + _last_timer_minute = minutes + _last_timer_second = displayed_second + timer_updated.emit(minutes, displayed_second) + + +# ── Pause control ────────────────────────────────────────────────────────── + +func toggle_pause() -> void: + if is_paused: + resume() + else: + pause() + + +func pause() -> void: + is_paused = true + get_tree().paused = true + paused.emit() + + +func resume() -> void: + is_paused = false + get_tree().paused = false + resumed.emit() + + +# ── Level reset ──────────────────────────────────────────────────────────── + +func reset_level() -> void: + """Call when restarting the current level to restore per-level state.""" + for node in get_tree().get_nodes_in_group("player_created"): + node.queue_free() + creations_remaining = creations_allowed + player_stamina = max_player_stamina + + +func reset_game() -> void: + """Call when starting a full new game.""" + if is_paused: + resume() + minutes = 0 + seconds = 0.0 + _last_timer_minute = -1 + _last_timer_second = -1 + diamonds = 0 + timer_updated.emit(0, 0) + reset_level() diff --git a/godot/autoloads/InputManager.gd b/godot/autoloads/InputManager.gd new file mode 100644 index 0000000..d40478c --- /dev/null +++ b/godot/autoloads/InputManager.gd @@ -0,0 +1,46 @@ +extends Node + +## Sluggers – InputManager (Autoload) +## +## Wraps Godot's built-in InputMap to expose human-readable action labels and +## support runtime key-rebinding (used by the Controls page of the pause menu). + +# ── Action registry ──────────────────────────────────────────────────────── + +## Maps action name → display label shown in the Controls menu. +const ACTION_LABELS: Dictionary = { + "move_left": "Move Left", + "move_right": "Move Right", + "jump": "Jump", + "create_solid": "Create Solid Block", + "create_bouncy":"Create Bouncy Block", + "pause": "Pause", + "restart": "Restart Level", +} + + +# ── Helpers ──────────────────────────────────────────────────────────────── + +func get_action_key_text(action: String) -> String: + """Returns a human-readable label for the first keyboard binding of action.""" + for event in InputMap.action_get_events(action): + if event is InputEventKey: + return event.as_text_keycode() + return "(unbound)" + + +func rebind_action(action: String, new_event: InputEventKey) -> void: + """Replaces the first keyboard event bound to action with new_event.""" + for event in InputMap.action_get_events(action): + if event is InputEventKey: + InputMap.action_erase_event(action, event) + break + InputMap.action_add_event(action, new_event) + + +func get_all_action_bindings() -> Dictionary: + """Returns a dictionary of action → current key label for all tracked actions.""" + var result: Dictionary = {} + for action in ACTION_LABELS: + result[action] = get_action_key_text(action) + return result diff --git a/godot/project.godot b/godot/project.godot new file mode 100644 index 0000000..ff9d623 --- /dev/null +++ b/godot/project.godot @@ -0,0 +1,68 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; but the file format allows using it in version control. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="Sluggers" +config/description="A construction-based 2D platformer" +config/version="0.1.0" +run/main_scene="res://scenes/rooms/Room0.tscn" +config/features=PackedStringArray("4.3", "GL Compatibility") + +[autoload] + +GameState="*res://autoloads/GameState.gd" +InputManager="*res://autoloads/InputManager.gd" + +[display] + +window/size/viewport_width=384 +window/size/viewport_height=216 +window/size/window_width_override=1152 +window/size/window_height_override=648 +window/stretch/mode="canvas_items" +window/stretch/aspect="keep" + +[input] + +move_left={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194319,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)] +} +move_right={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194321,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)] +} +jump={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194320,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)] +} +create_solid={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":90,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)] +} +create_bouncy={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":88,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)] +} +pause={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194305,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)] +} +restart={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":82,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)] +} + +[rendering] + +textures/canvas_textures/default_texture_filter=0 +renderer/rendering_method="gl_compatibility" +renderer/rendering_method.mobile="gl_compatibility" diff --git a/godot/scenes/camera/GameCamera.gd b/godot/scenes/camera/GameCamera.gd new file mode 100644 index 0000000..2acd141 --- /dev/null +++ b/godot/scenes/camera/GameCamera.gd @@ -0,0 +1,30 @@ +extends Camera2D + +## Sluggers – GameCamera +## +## Smoothly follows a target node using linear interpolation, preserving the +## same feel as the original GML camera that lerped at 0.1 per frame. +## The target is normally the Player node in the current room. +## Camera2D limit_* properties are respected: the lerp destination is clamped +## so the view never drifts outside the room boundaries. + +## NodePath to the node this camera should follow (set per-room in the editor). +@export var target_path: NodePath = NodePath("") +## Interpolation speed (0 = no movement, 1 = instant snap). +@export_range(0.0, 1.0, 0.01) var lerp_speed: float = 0.1 + +var _target: Node2D = null + + +func _ready() -> void: + if not target_path.is_empty(): + _target = get_node_or_null(target_path) + + +func _process(_delta: float) -> void: + if _target == null: + return + var half := get_viewport_rect().size * 0.5 + var tx := clampf(_target.global_position.x, limit_left + half.x, limit_right - half.x) + var ty := clampf(_target.global_position.y, limit_top + half.y, limit_bottom - half.y) + global_position = global_position.lerp(Vector2(tx, ty), lerp_speed) diff --git a/godot/scenes/camera/GameCamera.tscn b/godot/scenes/camera/GameCamera.tscn new file mode 100644 index 0000000..be4df57 --- /dev/null +++ b/godot/scenes/camera/GameCamera.tscn @@ -0,0 +1,7 @@ +[gd_scene load_steps=2 format=3 uid="uid://bgamecam001"] + +[ext_resource type="Script" path="res://scenes/camera/GameCamera.gd" id="1_gamecam"] + +[node name="GameCamera" type="Camera2D"] +zoom = Vector2(1, 1) +script = ExtResource("1_gamecam") diff --git a/godot/scenes/objects/BouncyCreation.gd b/godot/scenes/objects/BouncyCreation.gd new file mode 100644 index 0000000..e662218 --- /dev/null +++ b/godot/scenes/objects/BouncyCreation.gd @@ -0,0 +1,18 @@ +extends Area2D + +## Sluggers – BouncyCreation +## +## A player-placed bouncy platform. When a CharacterBody2D lands on it the +## body is launched upward with BOUNCE_FORCE, replicating the GML bounce object. +## Added to the "player_created" group for centralized cleanup on level reset. + +const BOUNCE_FORCE: float = -1500.0 + +func _ready() -> void: + add_to_group("player_created") + body_entered.connect(_on_body_entered) + + +func _on_body_entered(body: Node2D) -> void: + if body is CharacterBody2D: + body.velocity.y = BOUNCE_FORCE diff --git a/godot/scenes/objects/BouncyCreation.tscn b/godot/scenes/objects/BouncyCreation.tscn new file mode 100644 index 0000000..485f90d --- /dev/null +++ b/godot/scenes/objects/BouncyCreation.tscn @@ -0,0 +1,17 @@ +[gd_scene load_steps=4 format=3 uid="uid://bbouncycr001"] + +[ext_resource type="Script" path="res://scenes/objects/BouncyCreation.gd" id="1_bouncycr"] +[ext_resource type="Texture2D" path="res://assets/sprites/s_creation_block.png" id="2_tex"] + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_bouncy"] +size = Vector2(32, 32) + +[node name="BouncyCreation" type="Area2D"] +script = ExtResource("1_bouncycr") + +[node name="CollisionShape2D" type="CollisionShape2D" parent="."] +shape = SubResource("RectangleShape2D_bouncy") + +[node name="Sprite" type="Sprite2D" parent="."] +texture = ExtResource("2_tex") +modulate = Color(1.0, 0.3, 0.1, 1) diff --git a/godot/scenes/objects/DeathArea.gd b/godot/scenes/objects/DeathArea.gd new file mode 100644 index 0000000..3462efa --- /dev/null +++ b/godot/scenes/objects/DeathArea.gd @@ -0,0 +1,18 @@ +extends Area2D + +## Sluggers – DeathArea +## +## An invisible kill zone (e.g. a pit or spike). Instantly kills the +## player on contact, using the player death/respawn flow when available. + +func _ready() -> void: + body_entered.connect(_on_body_entered) + + +func _on_body_entered(body: Node2D) -> void: + if body.is_in_group("player"): + 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/DeathArea.tscn b/godot/scenes/objects/DeathArea.tscn new file mode 100644 index 0000000..6a0f265 --- /dev/null +++ b/godot/scenes/objects/DeathArea.tscn @@ -0,0 +1,12 @@ +[gd_scene load_steps=4 format=3 uid="uid://bdeatha001"] + +[ext_resource type="Script" path="res://scenes/objects/DeathArea.gd" id="1_deatha"] + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_deatha"] +size = Vector2(2000, 16) + +[node name="DeathArea" type="Area2D"] +script = ExtResource("1_deatha") + +[node name="CollisionShape2D" type="CollisionShape2D" parent="."] +shape = SubResource("RectangleShape2D_deatha") diff --git a/godot/scenes/objects/Diamond.gd b/godot/scenes/objects/Diamond.gd new file mode 100644 index 0000000..4e33aa7 --- /dev/null +++ b/godot/scenes/objects/Diamond.gd @@ -0,0 +1,15 @@ +extends Area2D + +## Sluggers – Diamond +## +## Collectible gem. Increments GameState.diamonds when touched by the player +## and then removes itself from the scene. + +func _ready() -> void: + body_entered.connect(_on_body_entered) + + +func _on_body_entered(body: Node2D) -> void: + if body.is_in_group("player"): + GameState.diamonds += 1 + queue_free() diff --git a/godot/scenes/objects/Diamond.tscn b/godot/scenes/objects/Diamond.tscn new file mode 100644 index 0000000..a7952f5 --- /dev/null +++ b/godot/scenes/objects/Diamond.tscn @@ -0,0 +1,54 @@ +[gd_scene load_steps=14 format=3 uid="uid://bdiamond001"] + +[ext_resource type="Script" path="res://scenes/objects/Diamond.gd" id="1_diamond"] +[ext_resource type="Texture2D" path="res://assets/sprites/s_diamond_0.png" id="2_d0"] +[ext_resource type="Texture2D" path="res://assets/sprites/s_diamond_1.png" id="3_d1"] +[ext_resource type="Texture2D" path="res://assets/sprites/s_diamond_2.png" id="4_d2"] +[ext_resource type="Texture2D" path="res://assets/sprites/s_diamond_3.png" id="5_d3"] +[ext_resource type="Texture2D" path="res://assets/sprites/s_diamond_4.png" id="6_d4"] +[ext_resource type="Texture2D" path="res://assets/sprites/s_diamond_5.png" id="7_d5"] +[ext_resource type="Texture2D" path="res://assets/sprites/s_diamond_6.png" id="8_d6"] +[ext_resource type="Texture2D" path="res://assets/sprites/s_diamond_7.png" id="9_d7"] +[ext_resource type="Texture2D" path="res://assets/sprites/s_diamond_8.png" id="10_d8"] +[ext_resource type="Texture2D" path="res://assets/sprites/s_diamond_9.png" id="11_d9"] + +[sub_resource type="CircleShape2D" id="CircleShape2D_diamond"] +radius = 12.0 + +[sub_resource type="SpriteFrames" id="SpriteFrames_diamond"] +animations = [{ +"frames": [{"duration": 1.0, +"texture": ExtResource("2_d0")}, { +"duration": 1.0, +"texture": ExtResource("3_d1")}, { +"duration": 1.0, +"texture": ExtResource("4_d2")}, { +"duration": 1.0, +"texture": ExtResource("5_d3")}, { +"duration": 1.0, +"texture": ExtResource("6_d4")}, { +"duration": 1.0, +"texture": ExtResource("7_d5")}, { +"duration": 1.0, +"texture": ExtResource("8_d6")}, { +"duration": 1.0, +"texture": ExtResource("9_d7")}, { +"duration": 1.0, +"texture": ExtResource("10_d8")}, { +"duration": 1.0, +"texture": ExtResource("11_d9")}], +"loop": true, +"name": &"spin", +"speed": 12.0 +}] + +[node name="Diamond" type="Area2D"] +script = ExtResource("1_diamond") + +[node name="CollisionShape2D" type="CollisionShape2D" parent="."] +shape = SubResource("CircleShape2D_diamond") + +[node name="Sprite" type="AnimatedSprite2D" parent="."] +sprite_frames = SubResource("SpriteFrames_diamond") +animation = &"spin" +autoplay = "spin" diff --git a/godot/scenes/objects/Enemy.gd b/godot/scenes/objects/Enemy.gd new file mode 100644 index 0000000..00f6e17 --- /dev/null +++ b/godot/scenes/objects/Enemy.gd @@ -0,0 +1,103 @@ +extends Area2D + +## Sluggers – Enemy +## +## A hazard that patrols horizontally between x_min and x_max. +## 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 + _direction = -1 + _sprite.flip_h = true + elif position.x <= x_min: + position.x = x_min + _direction = 1 + _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 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 new file mode 100644 index 0000000..2de4280 --- /dev/null +++ b/godot/scenes/objects/Enemy.tscn @@ -0,0 +1,41 @@ +[gd_scene load_steps=6 format=3 uid="uid://benemy001"] + +[ext_resource type="Script" path="res://scenes/objects/Enemy.gd" id="1_enemy"] +[ext_resource type="Texture2D" path="res://assets/sprites/s_enemy_0.png" id="2_e0"] +[ext_resource type="Texture2D" path="res://assets/sprites/s_enemy_1.png" id="3_e1"] + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_enemy"] +size = Vector2(20, 48) + +[sub_resource type="SpriteFrames" id="SpriteFrames_enemy"] +animations = [{ +"frames": [{"duration": 1.0, +"texture": ExtResource("2_e0")}, { +"duration": 1.0, +"texture": ExtResource("3_e1")}], +"loop": true, +"name": &"default", +"speed": 6.0 +}] + +[node name="Enemy" type="Area2D"] +script = ExtResource("1_enemy") + +[node name="CollisionShape2D" type="CollisionShape2D" parent="."] +position = Vector2(0, -24) +shape = SubResource("RectangleShape2D_enemy") + +[node name="Sprite" type="AnimatedSprite2D" parent="."] +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/objects/MovingBlock.gd b/godot/scenes/objects/MovingBlock.gd new file mode 100644 index 0000000..98b42b6 --- /dev/null +++ b/godot/scenes/objects/MovingBlock.gd @@ -0,0 +1,24 @@ +extends AnimatableBody2D + +## Sluggers – MovingBlock +## +## A horizontal platform that shuttles back and forth between x_min and x_max. +## Uses AnimatableBody2D so that CharacterBody2D nodes riding on top are +## correctly carried along (Godot's built-in platform-riding logic). + +@export var x_min: float = 400.0 +@export var x_max: float = 1000.0 +## Movement speed in pixels per second. +@export var speed: float = 300.0 + +var _direction: int = 1 + + +func _physics_process(delta: float) -> void: + position.x += speed * _direction * delta + if position.x >= x_max: + position.x = x_max + _direction = -1 + elif position.x <= x_min: + position.x = x_min + _direction = 1 diff --git a/godot/scenes/objects/MovingBlock.tscn b/godot/scenes/objects/MovingBlock.tscn new file mode 100644 index 0000000..66251a5 --- /dev/null +++ b/godot/scenes/objects/MovingBlock.tscn @@ -0,0 +1,19 @@ +[gd_scene load_steps=4 format=3 uid="uid://bmovingb001"] + +[ext_resource type="Script" path="res://scenes/objects/MovingBlock.gd" id="1_movingb"] + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_movingb"] +size = Vector2(64, 16) + +[node name="MovingBlock" type="AnimatableBody2D"] +script = ExtResource("1_movingb") + +[node name="CollisionShape2D" type="CollisionShape2D" parent="."] +shape = SubResource("RectangleShape2D_movingb") + +[node name="Visual" type="ColorRect" parent="."] +offset_left = -32.0 +offset_top = -8.0 +offset_right = 32.0 +offset_bottom = 8.0 +color = Color(0.2, 0.8, 0.2, 1) diff --git a/godot/scenes/objects/RoomWarp.gd b/godot/scenes/objects/RoomWarp.gd new file mode 100644 index 0000000..6c7e5d0 --- /dev/null +++ b/godot/scenes/objects/RoomWarp.gd @@ -0,0 +1,19 @@ +extends Area2D + +## Sluggers – RoomWarp +## +## Transitions the game to target_scene when the player enters this area. +## The player's spawn position in the destination room is controlled by +## placing the RoomWarp node near the matching entry point in that scene. + +## Path to the destination scene (*.tscn), set in the editor. +@export_file("*.tscn") var target_scene: String = "" + + +func _ready() -> void: + body_entered.connect(_on_body_entered) + + +func _on_body_entered(body: Node2D) -> void: + if body.is_in_group("player") and not target_scene.is_empty(): + get_tree().change_scene_to_file(target_scene) diff --git a/godot/scenes/objects/RoomWarp.tscn b/godot/scenes/objects/RoomWarp.tscn new file mode 100644 index 0000000..2f39447 --- /dev/null +++ b/godot/scenes/objects/RoomWarp.tscn @@ -0,0 +1,19 @@ +[gd_scene load_steps=4 format=3 uid="uid://broomwarp001"] + +[ext_resource type="Script" path="res://scenes/objects/RoomWarp.gd" id="1_roomwarp"] +[ext_resource type="Texture2D" path="res://assets/sprites/s_room_warp.png" id="2_tex"] + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_warp"] +size = Vector2(32, 48) + +[node name="RoomWarp" type="Area2D"] +script = ExtResource("1_roomwarp") + +[node name="CollisionShape2D" type="CollisionShape2D" parent="."] +position = Vector2(0, -24) +shape = SubResource("RectangleShape2D_warp") + +[node name="Sprite" type="Sprite2D" parent="."] +position = Vector2(0, -31) +texture = ExtResource("2_tex") +modulate = Color(1.0, 1.0, 1.0, 0.8) diff --git a/godot/scenes/objects/Solid.tscn b/godot/scenes/objects/Solid.tscn new file mode 100644 index 0000000..f41190f --- /dev/null +++ b/godot/scenes/objects/Solid.tscn @@ -0,0 +1,16 @@ +[gd_scene load_steps=4 format=3 uid="uid://bsolid001"] + +[ext_resource type="Texture2D" path="res://assets/sprites/s_solid.png" id="1_tex"] + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_solid"] +size = Vector2(64, 64) + +[node name="Solid" type="StaticBody2D"] + +[node name="CollisionShape2D" type="CollisionShape2D" parent="."] +shape = SubResource("RectangleShape2D_solid") + +[node name="Sprite" type="Sprite2D" parent="."] +position = Vector2(32, 32) +centered = false +texture = ExtResource("1_tex") diff --git a/godot/scenes/objects/SolidCreation.gd b/godot/scenes/objects/SolidCreation.gd new file mode 100644 index 0000000..6056db6 --- /dev/null +++ b/godot/scenes/objects/SolidCreation.gd @@ -0,0 +1,9 @@ +extends StaticBody2D + +## Sluggers – SolidCreation +## +## A player-placed solid platform. Acts as standard terrain once placed. +## Added to the "player_created" group for centralized cleanup on level reset. + +func _ready() -> void: + add_to_group("player_created") diff --git a/godot/scenes/objects/SolidCreation.tscn b/godot/scenes/objects/SolidCreation.tscn new file mode 100644 index 0000000..bec4ac7 --- /dev/null +++ b/godot/scenes/objects/SolidCreation.tscn @@ -0,0 +1,16 @@ +[gd_scene load_steps=4 format=3 uid="uid://bsolidcr001"] + +[ext_resource type="Script" path="res://scenes/objects/SolidCreation.gd" id="1_solidcr"] +[ext_resource type="Texture2D" path="res://assets/sprites/s_creation_block.png" id="2_tex"] + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_solidcr"] +size = Vector2(32, 32) + +[node name="SolidCreation" type="StaticBody2D"] +script = ExtResource("1_solidcr") + +[node name="CollisionShape2D" type="CollisionShape2D" parent="."] +shape = SubResource("RectangleShape2D_solidcr") + +[node name="Sprite" type="Sprite2D" parent="."] +texture = ExtResource("2_tex") diff --git a/godot/scenes/player/Player.gd b/godot/scenes/player/Player.gd new file mode 100644 index 0000000..1414659 --- /dev/null +++ b/godot/scenes/player/Player.gd @@ -0,0 +1,403 @@ +extends CharacterBody2D + +## Sluggers – Player +## +## Physics-based platformer character with movement states: +## GROUND – walking, with friction-based deceleration +## 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²) ──── +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² (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 +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, DEAD } +var _state: State = State.AIR + +## Direction of the wall the player is currently touching (-1 = left, +1 = right). +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 ─────────────────────────────────────────────────────── +@export var solid_creation_scene: PackedScene +@export var bouncy_creation_scene: PackedScene + +@onready var _sprite: AnimatedSprite2D = $Sprite + + +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 ────────────────────────────────────────────────────────────────── + +func _handle_meta_input() -> void: + if Input.is_action_just_pressed("pause"): + GameState.toggle_pause() + if Input.is_action_just_pressed("restart"): + GameState.reset_level() + get_tree().reload_current_scene() + + +func _handle_movement_input(delta: float) -> void: + var left := Input.is_action_pressed("move_left") + var right := Input.is_action_pressed("move_right") + + # 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_buffer_timer > 0.0: + _start_jump() + _jump_buffer_timer = 0.0 + + State.AIR: + _air_move(left, right, delta) + # 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_buffer_timer > 0.0: + _wall_jump() + _jump_buffer_timer = 0.0 + _handle_creation_input() + + if _state == State.GROUND: + _handle_creation_input() + + +func _handle_creation_input() -> void: + if GameState.creations_remaining <= 0: + return + if Input.is_action_just_pressed("create_solid") and solid_creation_scene: + _place_block(solid_creation_scene) + elif Input.is_action_just_pressed("create_bouncy") and bouncy_creation_scene: + _place_block(bouncy_creation_scene) + + +# ── Movement helpers ─────────────────────────────────────────────────────── + +func _ground_move(left: bool, right: bool, delta: float) -> void: + if right and not left: + velocity.x = _accelerate_toward(velocity.x, MAX_SPEED, ACCELERATION * delta) + _facing = 1 + elif left and not right: + velocity.x = _accelerate_toward(velocity.x, -MAX_SPEED, ACCELERATION * delta) + _facing = -1 + else: + velocity.x = _apply_friction(velocity.x, FRICTION_COEFF) + _update_facing_visual() + + +func _air_move(left: bool, right: bool, delta: float) -> void: + # 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 + elif left and not right: + velocity.x = _accelerate_toward(velocity.x, -MAX_SPEED, air_accel) + _facing = -1 + _update_facing_visual() + + +func _apply_wall_friction() -> void: + if velocity.y > 0.0: + velocity.y = _apply_friction(velocity.y, WALL_FRICTION_COEFF) + + +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 + # 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 ──────────────────────────────────────────────────────────────── + +func _apply_gravity(delta: float) -> void: + if _state != State.GROUND: + velocity.y = minf(velocity.y + GRAVITY * delta, MAX_FALL_SPEED) + + +func _execute_movement() -> void: + move_and_slide() + + +# ── State machine ────────────────────────────────────────────────────────── + +func _update_state() -> void: + var was_grounded := (_state == State.GROUND) + + if is_on_floor(): + _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 + + +func _detect_wall_direction() -> void: + for i in get_slide_collision_count(): + var col := get_slide_collision(i) + _wall_dir = -int(sign(col.get_normal().x)) + 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: + var place_pos := position + Vector2(_facing * BLOCK_PLACE_OFFSET, 0.0) + + # Refuse to place if the target cell overlaps existing physics geometry. + var query := PhysicsPointQueryParameters2D.new() + query.position = place_pos + query.exclude = [self] + if not get_world_2d().direct_space_state.intersect_point(query).is_empty(): + return + + var block := scene.instantiate() as Node2D + block.position = place_pos + get_tree().current_scene.add_child(block) + GameState.creations_remaining -= 1 + + +# ── Visual helpers ───────────────────────────────────────────────────────── + +func _update_facing_visual() -> void: + if _sprite: + _sprite.flip_h = (_facing == -1) + + +func _update_animation() -> void: + if not _sprite: + return + match _state: + State.GROUND: + if absf(velocity.x) > 10.0: + _sprite.play("walk") + else: + _sprite.play("idle") + State.AIR: + _sprite.play("jump") + State.WALL: + _sprite.play("slide") + + +# ── Pure utility functions (mirrors of GML script equivalents) ───────────── + +## Moves `current` toward `target` by at most `increment` per call. +## Equivalent to GML approach(). +static func _approach(current: float, target: float, increment: float) -> float: + if current < target: + return minf(current + increment, target) + return maxf(current - increment, target) + + +## Accelerates `current` toward `±max_spd` by `accel` per call. +## Equivalent to GML add_movement_maxspeed(). +static func _accelerate_toward(current: float, target_spd: float, accel: float) -> float: + var diff := target_spd - current + if absf(diff) <= accel: + return target_spd + return current + accel * signf(diff) + + +## Applies a proportional friction force, bringing `value` toward zero. +## Equivalent to GML apply_friction_to_movement_entity(). +static func _apply_friction(value: float, coeff: float) -> float: + var drag := coeff * absf(value) + if absf(value) <= drag: + return 0.0 + return value - signf(value) * drag diff --git a/godot/scenes/player/Player.tscn b/godot/scenes/player/Player.tscn new file mode 100644 index 0000000..e3c4a6d --- /dev/null +++ b/godot/scenes/player/Player.tscn @@ -0,0 +1,77 @@ +[gd_scene load_steps=19 format=3 uid="uid://bplayer001"] + +[ext_resource type="Script" path="res://scenes/player/Player.gd" id="1_player"] +[ext_resource type="Texture2D" path="res://assets/sprites/s_player_idle_0.png" id="2_pi0"] +[ext_resource type="Texture2D" path="res://assets/sprites/s_player_idle_1.png" id="3_pi1"] +[ext_resource type="Texture2D" path="res://assets/sprites/s_player_idle_2.png" id="4_pi2"] +[ext_resource type="Texture2D" path="res://assets/sprites/s_player_walk_0.png" id="5_pw0"] +[ext_resource type="Texture2D" path="res://assets/sprites/s_player_walk_1.png" id="6_pw1"] +[ext_resource type="Texture2D" path="res://assets/sprites/s_player_walk_2.png" id="7_pw2"] +[ext_resource type="Texture2D" path="res://assets/sprites/s_player_walk_3.png" id="8_pw3"] +[ext_resource type="Texture2D" path="res://assets/sprites/s_player_walk_4.png" id="9_pw4"] +[ext_resource type="Texture2D" path="res://assets/sprites/s_player_walk_5.png" id="10_pw5"] +[ext_resource type="Texture2D" path="res://assets/sprites/s_player_walk_6.png" id="11_pw6"] +[ext_resource type="Texture2D" path="res://assets/sprites/s_player_jump_0.png" id="12_pj0"] +[ext_resource type="Texture2D" path="res://assets/sprites/s_player_jump_1.png" id="13_pj1"] +[ext_resource type="Texture2D" path="res://assets/sprites/s_player_slide.png" id="14_pslide"] + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_body"] +size = Vector2(16, 28) + +[sub_resource type="SpriteFrames" id="SpriteFrames_player"] +animations = [{ +"frames": [{"duration": 1.0, +"texture": ExtResource("2_pi0")}, { +"duration": 1.0, +"texture": ExtResource("3_pi1")}, { +"duration": 1.0, +"texture": ExtResource("4_pi2")}], +"loop": true, +"name": &"idle", +"speed": 8.0 +}, { +"frames": [{"duration": 1.0, +"texture": ExtResource("5_pw0")}, { +"duration": 1.0, +"texture": ExtResource("6_pw1")}, { +"duration": 1.0, +"texture": ExtResource("7_pw2")}, { +"duration": 1.0, +"texture": ExtResource("8_pw3")}, { +"duration": 1.0, +"texture": ExtResource("9_pw4")}, { +"duration": 1.0, +"texture": ExtResource("10_pw5")}, { +"duration": 1.0, +"texture": ExtResource("11_pw6")}], +"loop": true, +"name": &"walk", +"speed": 10.0 +}, { +"frames": [{"duration": 1.0, +"texture": ExtResource("12_pj0")}, { +"duration": 1.0, +"texture": ExtResource("13_pj1")}], +"loop": true, +"name": &"jump", +"speed": 8.0 +}, { +"frames": [{"duration": 1.0, +"texture": ExtResource("14_pslide")}], +"loop": true, +"name": &"slide", +"speed": 5.0 +}] + +[node name="Player" type="CharacterBody2D"] +script = ExtResource("1_player") + +[node name="CollisionShape2D" type="CollisionShape2D" parent="."] +position = Vector2(0, -14) +shape = SubResource("RectangleShape2D_body") + +[node name="Sprite" type="AnimatedSprite2D" parent="."] +position = Vector2(0, -31) +sprite_frames = SubResource("SpriteFrames_player") +animation = &"idle" +autoplay = "idle" diff --git a/godot/scenes/rooms/Room0.tscn b/godot/scenes/rooms/Room0.tscn new file mode 100644 index 0000000..120ef33 --- /dev/null +++ b/godot/scenes/rooms/Room0.tscn @@ -0,0 +1,260 @@ +[gd_scene load_steps=28 format=3 uid="uid://broom0001"] + +[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_plat1"] +size = Vector2(80, 16) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_plat2"] +size = Vector2(80, 16) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_plat3"] +size = Vector2(64, 16) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_plat4"] +size = Vector2(96, 16) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_plat5"] +size = Vector2(80, 16) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_plat6"] +size = Vector2(64, 16) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_wallL"] +size = Vector2(16, 216) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_wallR"] +size = Vector2(16, 216) + +; ── Root ────────────────────────────────────────────────────────────────── +[node name="Room0" type="Node2D"] + +; ── Sky 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.35, 0.25, 0.15, 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.35, 0.25, 0.15, 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.35, 0.25, 0.15, 1) + +[node name="Platform1" type="StaticBody2D" parent="Terrain"] +position = Vector2(112, 168) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Terrain/Platform1"] +shape = SubResource("RectangleShape2D_plat1") + +[node name="Visual" type="ColorRect" parent="Terrain/Platform1"] +offset_left = -40.0 +offset_top = -8.0 +offset_right = 40.0 +offset_bottom = 8.0 +color = Color(0.35, 0.25, 0.15, 1) + +[node name="Platform2" type="StaticBody2D" parent="Terrain"] +position = Vector2(256, 136) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Terrain/Platform2"] +shape = SubResource("RectangleShape2D_plat2") + +[node name="Visual" type="ColorRect" parent="Terrain/Platform2"] +offset_left = -40.0 +offset_top = -8.0 +offset_right = 40.0 +offset_bottom = 8.0 +color = Color(0.35, 0.25, 0.15, 1) + +[node name="Platform3" type="StaticBody2D" parent="Terrain"] +position = Vector2(192, 96) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Terrain/Platform3"] +shape = SubResource("RectangleShape2D_plat3") + +[node name="Visual" type="ColorRect" parent="Terrain/Platform3"] +offset_left = -32.0 +offset_top = -8.0 +offset_right = 32.0 +offset_bottom = 8.0 +color = Color(0.35, 0.25, 0.15, 1) + +; ── Second-screen platforms ──────────────────────────────────────────────── +[node name="Platform4" type="StaticBody2D" parent="Terrain"] +position = Vector2(480, 168) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Terrain/Platform4"] +shape = SubResource("RectangleShape2D_plat4") + +[node name="Visual" type="ColorRect" parent="Terrain/Platform4"] +offset_left = -48.0 +offset_top = -8.0 +offset_right = 48.0 +offset_bottom = 8.0 +color = Color(0.35, 0.25, 0.15, 1) + +[node name="Platform5" type="StaticBody2D" parent="Terrain"] +position = Vector2(608, 136) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Terrain/Platform5"] +shape = SubResource("RectangleShape2D_plat5") + +[node name="Visual" type="ColorRect" parent="Terrain/Platform5"] +offset_left = -40.0 +offset_top = -8.0 +offset_right = 40.0 +offset_bottom = 8.0 +color = Color(0.35, 0.25, 0.15, 1) + +[node name="Platform6" type="StaticBody2D" parent="Terrain"] +position = Vector2(672, 96) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Terrain/Platform6"] +shape = SubResource("RectangleShape2D_plat6") + +[node name="Visual" type="ColorRect" parent="Terrain/Platform6"] +offset_left = -32.0 +offset_top = -8.0 +offset_right = 32.0 +offset_bottom = 8.0 +color = Color(0.35, 0.25, 0.15, 1) + +; ── Death pit (below the floor) ──────────────────────────────────────────── +[node name="DeathPit" parent="." instance=ExtResource("4_deatha")] +position = Vector2(384, 224) + +; ── Collectibles ─────────────────────────────────────────────────────────── +[node name="Diamond1" parent="." instance=ExtResource("2_diamond")] +position = Vector2(112, 152) + +[node name="Diamond2" parent="." instance=ExtResource("2_diamond")] +position = Vector2(256, 120) + +[node name="Diamond3" parent="." instance=ExtResource("2_diamond")] +position = Vector2(192, 80) + +[node name="Diamond4" parent="." instance=ExtResource("2_diamond")] +position = Vector2(480, 152) + +[node name="Diamond5" parent="." instance=ExtResource("2_diamond")] +position = Vector2(608, 120) + +[node name="Diamond6" parent="." instance=ExtResource("2_diamond")] +position = Vector2(672, 80) + +; ── Moving platforms ─────────────────────────────────────────────────────── +[node name="MovingBlock" parent="." instance=ExtResource("3_movingb")] +position = Vector2(80, 168) +x_min = 32.0 +x_max = 160.0 +speed = 200.0 + +[node name="MovingBlock2" parent="." instance=ExtResource("3_movingb")] +position = Vector2(512, 168) +x_min = 400.0 +x_max = 592.0 +speed = 200.0 + +; ── Enemies ──────────────────────────────────────────────────────────────── +[node name="Enemy1" parent="." instance=ExtResource("6_enemy")] +position = Vector2(320, 200) +x_min = 256.0 +x_max = 384.0 + +[node name="Enemy2" parent="." instance=ExtResource("6_enemy")] +position = Vector2(640, 200) +x_min = 576.0 +x_max = 720.0 + +; ── Room warp (leads to Room 1) ──────────────────────────────────────────── +[node name="RoomWarp" parent="." instance=ExtResource("5_warp")] +position = Vector2(752, 192) +target_scene = "res://scenes/rooms/Room1.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/Room1.tscn b/godot/scenes/rooms/Room1.tscn new file mode 100644 index 0000000..26fa136 --- /dev/null +++ b/godot/scenes/rooms/Room1.tscn @@ -0,0 +1,276 @@ +[gd_scene load_steps=24 format=3 uid="uid://broom1001"] + +[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_plat1"] +size = Vector2(64, 16) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_plat2"] +size = Vector2(64, 16) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_plat3"] +size = Vector2(96, 16) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_plat4"] +size = Vector2(64, 16) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_plat5"] +size = Vector2(64, 16) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_plat6"] +size = Vector2(64, 16) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_wallL"] +size = Vector2(16, 216) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_wallR"] +size = Vector2(16, 216) + +; ── Root ────────────────────────────────────────────────────────────────── +[node name="Room1" type="Node2D"] + +; ── Sky 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.3, 0.2, 0.1, 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.3, 0.2, 0.1, 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.3, 0.2, 0.1, 1) + +; Staggered platforms – harder layout than Room 0 +[node name="Platform1" type="StaticBody2D" parent="Terrain"] +position = Vector2(80, 176) + +[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.3, 0.2, 0.1, 1) + +[node name="Platform2" type="StaticBody2D" parent="Terrain"] +position = Vector2(256, 148) + +[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.3, 0.2, 0.1, 1) + +[node name="Platform3" type="StaticBody2D" parent="Terrain"] +position = Vector2(400, 112) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Terrain/Platform3"] +shape = SubResource("RectangleShape2D_plat3") + +[node name="Visual" type="ColorRect" parent="Terrain/Platform3"] +offset_left = -48.0 +offset_top = -8.0 +offset_right = 48.0 +offset_bottom = 8.0 +color = Color(0.3, 0.2, 0.1, 1) + +[node name="Platform4" type="StaticBody2D" parent="Terrain"] +position = Vector2(512, 176) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Terrain/Platform4"] +shape = SubResource("RectangleShape2D_plat4") + +[node name="Visual" type="ColorRect" parent="Terrain/Platform4"] +offset_left = -32.0 +offset_top = -8.0 +offset_right = 32.0 +offset_bottom = 8.0 +color = Color(0.3, 0.2, 0.1, 1) + +[node name="Platform5" type="StaticBody2D" parent="Terrain"] +position = Vector2(640, 148) + +[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.3, 0.2, 0.1, 1) + +[node name="Platform6" type="StaticBody2D" parent="Terrain"] +position = Vector2(700, 112) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Terrain/Platform6"] +shape = SubResource("RectangleShape2D_plat6") + +[node name="Visual" type="ColorRect" parent="Terrain/Platform6"] +offset_left = -32.0 +offset_top = -8.0 +offset_right = 32.0 +offset_bottom = 8.0 +color = Color(0.3, 0.2, 0.1, 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, 160) + +[node name="Diamond2" parent="." instance=ExtResource("2_diamond")] +position = Vector2(256, 132) + +[node name="Diamond3" parent="." instance=ExtResource("2_diamond")] +position = Vector2(400, 96) + +[node name="Diamond4" parent="." instance=ExtResource("2_diamond")] +position = Vector2(192, 48) + +[node name="Diamond5" parent="." instance=ExtResource("2_diamond")] +position = Vector2(512, 160) + +[node name="Diamond6" parent="." instance=ExtResource("2_diamond")] +position = Vector2(640, 132) + +; ── Moving platforms ─────────────────────────────────────────────────────── +[node name="MovingBlock1" parent="." instance=ExtResource("3_movingb")] +position = Vector2(128, 148) +x_min = 40.0 +x_max = 200.0 +speed = 200.0 + +[node name="MovingBlock2" parent="." instance=ExtResource("3_movingb")] +position = Vector2(320, 112) +x_min = 240.0 +x_max = 400.0 +speed = 250.0 + +[node name="MovingBlock3" parent="." instance=ExtResource("3_movingb")] +position = Vector2(550, 148) +x_min = 440.0 +x_max = 640.0 +speed = 250.0 + +; ── Enemies ──────────────────────────────────────────────────────────────── +[node name="Enemy1" parent="." instance=ExtResource("6_enemy")] +position = Vector2(160, 200) +x_min = 80.0 +x_max = 256.0 + +[node name="Enemy2" parent="." instance=ExtResource("6_enemy")] +position = Vector2(400, 200) +x_min = 320.0 +x_max = 480.0 + +[node name="Enemy3" parent="." instance=ExtResource("6_enemy")] +position = Vector2(650, 200) +x_min = 560.0 +x_max = 740.0 + +; ── Room warp back to Room 0 ─────────────────────────────────────────────── +[node name="RoomWarpBack" parent="." instance=ExtResource("5_warp")] +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) +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/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 new file mode 100644 index 0000000..f8a3ed6 --- /dev/null +++ b/godot/scenes/ui/HUD.gd @@ -0,0 +1,59 @@ +extends CanvasLayer + +## Sluggers – HUD +## +## 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 _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() + + +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: + _timer_label.text = "%02d:%02d" % [mins, secs] + + +func _on_diamonds_changed(count: int) -> void: + _diamond_label.text = "Diamonds: %d" % count + + +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 new file mode 100644 index 0000000..4be15cd --- /dev/null +++ b/godot/scenes/ui/HUD.tscn @@ -0,0 +1,56 @@ +[gd_scene load_steps=2 format=3 uid="uid://bhud001"] + +[ext_resource type="Script" path="res://scenes/ui/HUD.gd" id="1_hud"] + +[node name="HUD" type="CanvasLayer"] +layer = 10 +script = ExtResource("1_hud") + +[node name="StatsPanel" type="PanelContainer" parent="."] +offset_right = 190.0 +offset_bottom = 82.0 + +[node name="VBox" type="VBoxContainer" parent="StatsPanel"] +layout_mode = 2 + +[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="StatsPanel/VBox"] +layout_mode = 2 +text = "Diamonds: 0" +theme_override_font_sizes/font_size = 10 + +[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 diff --git a/godot/scenes/ui/PauseMenu.gd b/godot/scenes/ui/PauseMenu.gd new file mode 100644 index 0000000..0c21ec7 --- /dev/null +++ b/godot/scenes/ui/PauseMenu.gd @@ -0,0 +1,209 @@ +extends CanvasLayer + +## Sluggers – PauseMenu +## +## Multi-page pause overlay with the following pages: +## MAIN – Resume / Settings / Quit +## SETTINGS – Audio / Difficulty / Graphics / Controls +## AUDIO – Master, Music, and Sound-effects volume sliders +## DIFFICULTY – Enemy difficulty and ally strength +## GRAPHICS – Resolution and window mode +## CONTROLS – Rebindable key bindings +## +## The node tree is hidden until GameState emits the `paused` signal. +## All sub-pages live as VBoxContainer children of $Menu and are toggled +## by _navigate_to(). + +enum Page { MAIN, SETTINGS, AUDIO, DIFFICULTY, GRAPHICS, CONTROLS } + +@onready var _overlay: ColorRect = $Overlay +@onready var _main_page: VBoxContainer = $Menu/MainPage +@onready var _settings_page: VBoxContainer = $Menu/SettingsPage +@onready var _audio_page: VBoxContainer = $Menu/AudioPage +@onready var _difficulty_page: VBoxContainer = $Menu/DifficultyPage +@onready var _graphics_page: VBoxContainer = $Menu/GraphicsPage +@onready var _controls_page: VBoxContainer = $Menu/ControlsPage + +var _current_page: Page = Page.MAIN +## Tracks which action is waiting for a new key press during rebinding. +var _rebinding_action: String = "" + + +func _ready() -> void: + process_mode = Node.PROCESS_MODE_ALWAYS + GameState.paused.connect(_show_menu) + GameState.resumed.connect(_hide_menu) + _connect_ui_signals() + hide() + + +# ── Visibility ───────────────────────────────────────────────────────────── + +func _show_menu() -> void: + show() + _navigate_to(Page.MAIN) + + +func _hide_menu() -> void: + hide() + + +# ── Page navigation ──────────────────────────────────────────────────────── + +func _navigate_to(page: Page) -> void: + _current_page = page + _main_page.visible = page == Page.MAIN + _settings_page.visible = page == Page.SETTINGS + _audio_page.visible = page == Page.AUDIO + _difficulty_page.visible = page == Page.DIFFICULTY + _graphics_page.visible = page == Page.GRAPHICS + _controls_page.visible = page == Page.CONTROLS + if page == Page.CONTROLS: + _refresh_controls_page() + + +# ── MAIN PAGE signals ────────────────────────────────────────────────────── + +func _on_resume_pressed() -> void: + GameState.resume() + + +func _on_settings_pressed() -> void: + _navigate_to(Page.SETTINGS) + + +func _on_quit_pressed() -> void: + get_tree().quit() + + +# ── SETTINGS PAGE signals ────────────────────────────────────────────────── + +func _on_audio_pressed() -> void: + _navigate_to(Page.AUDIO) + + +func _on_difficulty_pressed() -> void: + _navigate_to(Page.DIFFICULTY) + + +func _on_graphics_pressed() -> void: + _navigate_to(Page.GRAPHICS) + + +func _on_controls_pressed() -> void: + _navigate_to(Page.CONTROLS) + + +func _on_back_pressed() -> void: + if _current_page == Page.MAIN: + GameState.resume() + else: + _navigate_to(Page.MAIN) + + +# ── AUDIO PAGE signals ───────────────────────────────────────────────────── + +func _on_master_volume_changed(value: float) -> void: + GameState.master_volume = value + _set_bus_volume("Master", value) + + +func _on_music_volume_changed(value: float) -> void: + GameState.music_volume = value + _set_bus_volume("Music", value) + + +func _on_sound_volume_changed(value: float) -> void: + GameState.sound_volume = value + _set_bus_volume("Sounds", value) + + +# ── DIFFICULTY PAGE signals ──────────────────────────────────────────────── + +func _on_difficulty_option_selected(index: int) -> void: + GameState.enemy_difficulty = index + + +# ── GRAPHICS PAGE signals ────────────────────────────────────────────────── + +func _on_fullscreen_toggled(button_pressed: bool) -> void: + if button_pressed: + DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN) + else: + DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED) + + +func _on_resolution_selected(index: int) -> void: + var resolutions: Array[Vector2i] = [ + Vector2i(768, 432), + Vector2i(1152, 648), + Vector2i(1536, 864), + Vector2i(1920, 1080), + ] + if index < resolutions.size(): + DisplayServer.window_set_size(resolutions[index]) + + +# ── CONTROLS PAGE ────────────────────────────────────────────────────────── + +func _refresh_controls_page() -> void: + """Update each button label to show the current binding.""" + var bindings := InputManager.get_all_action_bindings() + for child in _controls_page.get_children(): + if child is Button and child.has_meta("_action"): + var action: String = child.get_meta("_action") + child.text = "%s: %s" % [ + InputManager.ACTION_LABELS.get(action, action), + bindings.get(action, "(unbound)")] + + +func _on_rebind_button_pressed(action: String) -> void: + _rebinding_action = action + + +func _input(event: InputEvent) -> void: + if _rebinding_action.is_empty() or not visible: + return + if event is InputEventKey and event.pressed and not event.echo: + InputManager.rebind_action(_rebinding_action, event) + _rebinding_action = "" + _refresh_controls_page() + get_viewport().set_input_as_handled() + + +func _connect_ui_signals() -> void: + $Menu/MainPage/ResumeButton.pressed.connect(_on_resume_pressed) + $Menu/MainPage/SettingsButton.pressed.connect(_on_settings_pressed) + $Menu/MainPage/QuitButton.pressed.connect(_on_quit_pressed) + + $Menu/SettingsPage/AudioButton.pressed.connect(_on_audio_pressed) + $Menu/SettingsPage/DifficultyButton.pressed.connect(_on_difficulty_pressed) + $Menu/SettingsPage/GraphicsButton.pressed.connect(_on_graphics_pressed) + $Menu/SettingsPage/ControlsButton.pressed.connect(_on_controls_pressed) + $Menu/SettingsPage/BackButton_Settings.pressed.connect(_on_back_pressed) + + $Menu/AudioPage/MasterSlider.value_changed.connect(_on_master_volume_changed) + $Menu/AudioPage/MusicSlider.value_changed.connect(_on_music_volume_changed) + $Menu/AudioPage/SoundSlider.value_changed.connect(_on_sound_volume_changed) + $Menu/AudioPage/BackButton_Audio.pressed.connect(_on_back_pressed) + + $Menu/DifficultyPage/DifficultyOption.item_selected.connect(_on_difficulty_option_selected) + $Menu/DifficultyPage/BackButton_Difficulty.pressed.connect(_on_back_pressed) + + $Menu/GraphicsPage/FullscreenCheck.toggled.connect(_on_fullscreen_toggled) + $Menu/GraphicsPage/ResolutionOption.item_selected.connect(_on_resolution_selected) + $Menu/GraphicsPage/BackButton_Graphics.pressed.connect(_on_back_pressed) + + $Menu/ControlsPage/BackButton_Controls.pressed.connect(_on_back_pressed) + for child in _controls_page.get_children(): + if child is Button and child.has_meta("_action"): + var action: String = child.get_meta("_action") + child.pressed.connect(_on_rebind_button_pressed.bind(action)) + + +func _set_bus_volume(primary_bus: String, value: float) -> void: + var bus_index: int = AudioServer.get_bus_index(primary_bus) + if bus_index == -1: + bus_index = AudioServer.get_bus_index("Master") + if bus_index != -1: + AudioServer.set_bus_volume_db(bus_index, linear_to_db(value)) diff --git a/godot/scenes/ui/PauseMenu.tscn b/godot/scenes/ui/PauseMenu.tscn new file mode 100644 index 0000000..62dac43 --- /dev/null +++ b/godot/scenes/ui/PauseMenu.tscn @@ -0,0 +1,165 @@ +[gd_scene load_steps=2 format=3 uid="uid://bpausemenu001"] + +[ext_resource type="Script" path="res://scenes/ui/PauseMenu.gd" id="1_pausemenu"] + +[node name="PauseMenu" type="CanvasLayer"] +layer = 20 +script = ExtResource("1_pausemenu") + +[node name="Overlay" type="ColorRect" parent="."] +anchors_preset = 15 +color = Color(0, 0, 0, 0.6) + +[node name="Menu" type="CenterContainer" parent="."] +anchors_preset = 15 + +[node name="MainPage" type="VBoxContainer" parent="Menu"] +custom_minimum_size = Vector2(140, 0) + +[node name="ResumeButton" type="Button" parent="Menu/MainPage"] +layout_mode = 2 +text = "Resume" + +[node name="SettingsButton" type="Button" parent="Menu/MainPage"] +layout_mode = 2 +text = "Settings" + +[node name="QuitButton" type="Button" parent="Menu/MainPage"] +layout_mode = 2 +text = "Quit" + +[node name="SettingsPage" type="VBoxContainer" parent="Menu"] +custom_minimum_size = Vector2(140, 0) +visible = false + +[node name="AudioButton" type="Button" parent="Menu/SettingsPage"] +layout_mode = 2 +text = "Audio" + +[node name="DifficultyButton" type="Button" parent="Menu/SettingsPage"] +layout_mode = 2 +text = "Difficulty" + +[node name="GraphicsButton" type="Button" parent="Menu/SettingsPage"] +layout_mode = 2 +text = "Graphics" + +[node name="ControlsButton" type="Button" parent="Menu/SettingsPage"] +layout_mode = 2 +text = "Controls" + +[node name="BackButton_Settings" type="Button" parent="Menu/SettingsPage"] +layout_mode = 2 +text = "Back" + +[node name="AudioPage" type="VBoxContainer" parent="Menu"] +custom_minimum_size = Vector2(160, 0) +visible = false + +[node name="MasterLabel" type="Label" parent="Menu/AudioPage"] +layout_mode = 2 +text = "Master Volume" + +[node name="MasterSlider" type="HSlider" parent="Menu/AudioPage"] +layout_mode = 2 +min_value = 0.0 +max_value = 1.0 +step = 0.05 +value = 1.0 + +[node name="MusicLabel" type="Label" parent="Menu/AudioPage"] +layout_mode = 2 +text = "Music Volume" + +[node name="MusicSlider" type="HSlider" parent="Menu/AudioPage"] +layout_mode = 2 +min_value = 0.0 +max_value = 1.0 +step = 0.05 +value = 1.0 + +[node name="SoundLabel" type="Label" parent="Menu/AudioPage"] +layout_mode = 2 +text = "Sound Volume" + +[node name="SoundSlider" type="HSlider" parent="Menu/AudioPage"] +layout_mode = 2 +min_value = 0.0 +max_value = 1.0 +step = 0.05 +value = 1.0 + +[node name="BackButton_Audio" type="Button" parent="Menu/AudioPage"] +layout_mode = 2 +text = "Back" + +[node name="DifficultyPage" type="VBoxContainer" parent="Menu"] +custom_minimum_size = Vector2(160, 0) +visible = false + +[node name="DifficultyLabel" type="Label" parent="Menu/DifficultyPage"] +layout_mode = 2 +text = "Enemy Difficulty" + +[node name="DifficultyOption" type="OptionButton" parent="Menu/DifficultyPage"] +layout_mode = 2 + +[node name="BackButton_Difficulty" type="Button" parent="Menu/DifficultyPage"] +layout_mode = 2 +text = "Back" + +[node name="GraphicsPage" type="VBoxContainer" parent="Menu"] +custom_minimum_size = Vector2(160, 0) +visible = false + +[node name="ResolutionLabel" type="Label" parent="Menu/GraphicsPage"] +layout_mode = 2 +text = "Resolution" + +[node name="ResolutionOption" type="OptionButton" parent="Menu/GraphicsPage"] +layout_mode = 2 + +[node name="FullscreenCheck" type="CheckButton" parent="Menu/GraphicsPage"] +layout_mode = 2 +text = "Fullscreen" + +[node name="BackButton_Graphics" type="Button" parent="Menu/GraphicsPage"] +layout_mode = 2 +text = "Back" + +[node name="ControlsPage" type="VBoxContainer" parent="Menu"] +custom_minimum_size = Vector2(200, 0) +visible = false + +[node name="ControlsLabel" type="Label" parent="Menu/ControlsPage"] +layout_mode = 2 +text = "Key Bindings (click to rebind)" + +[node name="MoveLeftBtn" type="Button" parent="Menu/ControlsPage"] +layout_mode = 2 +text = "Move Left: Left" +metadata/_action = "move_left" + +[node name="MoveRightBtn" type="Button" parent="Menu/ControlsPage"] +layout_mode = 2 +text = "Move Right: Right" +metadata/_action = "move_right" + +[node name="JumpBtn" type="Button" parent="Menu/ControlsPage"] +layout_mode = 2 +text = "Jump: Up" +metadata/_action = "jump" + +[node name="CreateSolidBtn" type="Button" parent="Menu/ControlsPage"] +layout_mode = 2 +text = "Create Solid: Z" +metadata/_action = "create_solid" + +[node name="CreateBouncyBtn" type="Button" parent="Menu/ControlsPage"] +layout_mode = 2 +text = "Create Bouncy: X" +metadata/_action = "create_bouncy" + +[node name="BackButton_Controls" type="Button" parent="Menu/ControlsPage"] +layout_mode = 2 +text = "Back"