Contributing Guide

Step-by-step guides for adding content, fixing bugs, and modifying systems.

Quick Start

  1. Open the project in Godot 4 (4.4+ recommended)
  2. Run the greybox: Press F6 while mayo_breach_greybox.tscn is open
  3. Run tests: Open the GUT panel (bottom dock) and click "Run All"
  4. Read the architecture guide: Architecture
  5. Follow coding standards: Coding Standards

How to Add a New Enemy

1Create the enemy script
# scripts/levels/greybox_my_enemy.gd
extends GreyboxEnemyBase

func _get_enemy_id() -> String:
    return "greybox.my_enemy"

func _get_sprite_path() -> String:
    return "res://assets/sprites/enemies/my_enemy.png"

func _get_death_color() -> Color:
    return Color(0.5, 0.8, 1.0, 1)  # Light blue particles

func _get_drop_chance() -> float:
    return 0.4  # 40% health drop chance
2Create the scene
  1. Right-click in scenes/levels/ → New Scene
  2. Root node: CharacterBody2D
  3. Add child: Sprite2D (name it Sprite2D — the base class looks for this)
  4. Add child: CollisionShape2D with a CircleShape2D
  5. Attach script: scripts/levels/greybox_my_enemy.gd
  6. Save as scenes/levels/greybox_my_enemy.tscn
3Add to the spawner

Open scenes/levels/mayo_breach_greybox.tscn, find the Spawner node, and add your scene to the exported PackedScene slot.

4Test
  1. Open mayo_breach_greybox.tscn
  2. Press F6 (run current scene)
  3. Verify your enemy appears, chases the player, takes damage, and dies

How to Add a New Level

1Create the scene
  1. Right-click in scenes/levels/ → New Scene
  2. Root node: Node2D
  3. Add child: TileMap
  4. Add child: GreyboxPlayer (instance from scenes/player/greybox_player.tscn)
  5. Add child: Spawner (instance or inline with greybox_spawner.gd)
  6. Add child: CanvasLayer for UI
2Write the level script
# scripts/levels/my_level.gd
extends Node2D

const TILE_SIZE := 32

var _room := [
    [1, 1, 1, 1, 1],
    [1, 2, 0, 3, 1],
    [1, 0, 0, 0, 1],
    [1, 3, 0, 4, 1],
    [1, 1, 1, 1, 1],
]

@onready var _tilemap: TileMap = $TileMap
@onready var _player: CharacterBody2D = $GreyboxPlayer

func _ready() -> void:
    _build_tileset()
    _paint_room()
    _place_player()

func _build_tileset() -> void:
    # See mayo_breach_greybox.gd for full example
    pass

func _paint_room() -> void:
    for y in _room.size():
        for x in _room[y].size():
            _tilemap.set_cell(0, Vector2i(x, y), _room[y][x], Vector2i(0, 0))

func _place_player() -> void:
    for y in _room.size():
        for x in _room[y].size():
            if _room[y][x] == 2:
                _player.position = Vector2(x * TILE_SIZE + TILE_SIZE / 2, y * TILE_SIZE + TILE_SIZE / 2)
                return
3Wire up the title screen

In scripts/levels/title_screen.gd, change the scene path:

get_tree().change_scene_to_file("res://scenes/levels/my_level.tscn")

How to Add a New UI Panel

1Create the scene
  1. Right-click in scenes/ui/ → New Scene
  2. Root should be CanvasLayer (for overlays) or Control (for widgets)
  3. Pick appropriate layer number (see UI Stack)
  4. Add your UI elements (Label, Button, Panel, etc.)
2Write the script
# scripts/ui/my_panel.gd
extends CanvasLayer

@onready var _label: Label = $Label

func _ready() -> void:
    # Listen to game events, NOT direct node references
    Events.wave_started.connect(_on_wave_started)
    visible = false

func _on_wave_started(wave_index: int, _total: int) -> void:
    _label.text = "Wave %d started!" % wave_index
    visible = true
3Add to the main scene

Open scenes/main.tscn and instance your UI panel as a child of the root.

How the Signal Bus Works

All gameplay events flow through the Events autoload. This is how scripts talk to each other without knowing each other exists.

Emitting an event

# Anywhere in the codebase:
Events.player_damaged.emit(10, "enemy.swarmer")

Listening to an event

func _ready() -> void:
    Events.player_damaged.connect(_on_player_damaged)

func _on_player_damaged(amount: float, source_id: String) -> void:
    print("Player took ", amount, " damage from ", source_id)

Adding a new signal

  1. Open scripts/autoload/events.gd
  2. Add your signal: signal my_event(param: Type)
  3. Document who emits it and who listens

Testing

Running Tests

  1. Open the GUT panel in the Godot editor (bottom dock)
  2. Click "Run All"
  3. Or run from terminal: godot --headless -s addons/gut/gut_cmdln.gd

Writing a New Test

# tests/unit/test_my_feature.gd
extends GutTest

func test_player_can_take_damage():
    var player = preload("res://scenes/player/greybox_player.tscn").instantiate()
    add_child_autofree(player)
    
    var initial_hp = player.current_hp
    player._on_player_damaged(10, "test")
    
    assert_eq(player.current_hp, initial_hp - 10, "HP should decrease by damage amount")

Test Naming Convention

Code Review Checklist

Before submitting changes: