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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 86 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,86 @@
# sluggers
# 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/
Comment on lines +37 to +53
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

README’s project structure comments are now out of date: it says Enemy.* “restarts the level on contact”, but the new Enemy.gd applies damage/knockback and supports stomping. Also, the room list shows only Room0/Room1 even though Room2–Room4 are added.

Copilot uses AI. Check for mistakes.
└── 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 |
14 changes: 14 additions & 0 deletions godot/.gitignore
Original file line number Diff line number Diff line change
@@ -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
31 changes: 31 additions & 0 deletions godot/assets/README.md
Original file line number Diff line number Diff line change
@@ -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.
Binary file added godot/assets/sprites/s_bg.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added godot/assets/sprites/s_bg_clouds_behind.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added godot/assets/sprites/s_bg_clouds_front.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added godot/assets/sprites/s_bg_sky.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added godot/assets/sprites/s_creation_block.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added godot/assets/sprites/s_diamond_0.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added godot/assets/sprites/s_diamond_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added godot/assets/sprites/s_diamond_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added godot/assets/sprites/s_diamond_3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added godot/assets/sprites/s_diamond_4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added godot/assets/sprites/s_diamond_5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added godot/assets/sprites/s_diamond_6.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added godot/assets/sprites/s_diamond_7.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added godot/assets/sprites/s_diamond_8.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added godot/assets/sprites/s_diamond_9.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added godot/assets/sprites/s_enemy_0.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added godot/assets/sprites/s_enemy_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added godot/assets/sprites/s_fg.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added godot/assets/sprites/s_mg.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added godot/assets/sprites/s_mg_city.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added godot/assets/sprites/s_mg_silhoette.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added godot/assets/sprites/s_player_idle_0.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added godot/assets/sprites/s_player_idle_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added godot/assets/sprites/s_player_idle_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added godot/assets/sprites/s_player_jump_0.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added godot/assets/sprites/s_player_jump_1.png
Binary file added godot/assets/sprites/s_player_slide.png
Binary file added godot/assets/sprites/s_player_walk_0.png
Binary file added godot/assets/sprites/s_player_walk_1.png
Binary file added godot/assets/sprites/s_player_walk_2.png
Binary file added godot/assets/sprites/s_player_walk_3.png
Binary file added godot/assets/sprites/s_player_walk_4.png
Binary file added godot/assets/sprites/s_player_walk_5.png
Binary file added godot/assets/sprites/s_player_walk_6.png
Binary file added godot/assets/sprites/s_room_warp.png
Binary file added godot/assets/sprites/s_solid.png
112 changes: 112 additions & 0 deletions godot/autoloads/GameState.gd
Original file line number Diff line number Diff line change
@@ -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)

Comment on lines +29 to +33
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The diamonds property setter assigns to diamonds inside its own setter, which will recurse indefinitely in GDScript. Use the implicit field variable (or a separate backing variable) when setting the stored value, then emit the signal.

Copilot uses AI. Check for mistakes.
# ── 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)

Comment on lines +36 to +40
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same recursion issue as diamonds: the creations_remaining setter assigns to creations_remaining inside its own setter, causing infinite recursion. Use field (or a backing variable) before emitting creations_changed.

Copilot uses AI. Check for mistakes.
# ── 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
Comment on lines +42 to +46
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The player_stamina setter assigns to player_stamina inside its own setter, which will recurse. Use field = clampi(value, 0, max_player_stamina) (or a backing variable) before emitting health_changed.

Copilot uses AI. Check for mistakes.

# ── 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)

Comment on lines +57 to +69
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

timer_updated is only emitted when a full minute elapses. As a result, HUD timer text will not update during the minute. Consider emitting when the displayed second changes (e.g., track last emitted int(seconds)), or emit every frame and let the HUD format it.

Copilot uses AI. Check for mistakes.

# ── 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()
Comment on lines +80 to +89
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pause() sets get_tree().paused = true, but the pause menu/UI nodes need to be configured to keep processing input while paused (e.g., process_mode = PROCESS_MODE_ALWAYS). Without that, users may be unable to interact with the PauseMenu to resume or change settings.

Copilot uses AI. Check for mistakes.


# ── 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()
46 changes: 46 additions & 0 deletions godot/autoloads/InputManager.gd
Original file line number Diff line number Diff line change
@@ -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
68 changes: 68 additions & 0 deletions godot/project.godot
Original file line number Diff line number Diff line change
@@ -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"
30 changes: 30 additions & 0 deletions godot/scenes/camera/GameCamera.gd
Original file line number Diff line number Diff line change
@@ -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)
Loading