Files
draugnorak/docs/patterns.md

7.0 KiB

Draugnorak Code Patterns Documentation

This document describes the standard patterns used throughout the Draugnorak codebase.

Standard get_X_at(pos) Pattern

All world objects and creatures use a consistent lookup pattern for position-based queries.

Pattern Template

ClassName@ get_class_name_at(int pos) {
    for (uint i = 0; i < class_array.length(); i++) {
        if (class_array[i].position == pos) {
            return @class_array[i];
        }
    }
    return null;
}

Examples

  • Undead@ get_undead_at(int pos) - Find undead creature at position
  • Bandit@ get_bandit_at(int pos) - Find bandit at position
  • GroundGame@ get_ground_game_at(int pos) - Find ground game animal at position
  • FlyingCreature@ get_flying_creature_at(int pos) - Find flying creature at position
  • WorldFire@ get_fire_at(int pos) - Find fire at position
  • WorldSnare@ get_snare_at(int pos) - Find snare at position
  • WorldStream@ get_stream_at(int pos) - Find stream at position
  • WorldHerbGarden@ get_herb_garden_at_base() - Special case for base buildings

Usage Notes

  • Always returns null if no object found at position
  • Use handle return type @ClassName for proper object reference
  • Position is always an int (tile coordinate)
  • Forward iteration is standard: for (uint i = 0; i < array.length(); i++)

Creature Class Structure Pattern

All creature classes follow a consistent structure built on Creature base class.

Base Class Structure

class Creature {
    int position;
    int health;
    int max_health;
    int sound_handle;
    timer movement_timer;
    timer action_timer;

    void update() { /* override me */ }
    void destroy() { /* cleanup */ }
}

Creature Type Classes

All creatures inherit from Creature and add type-specific behavior:

  • Undead (zombies, vampires, ghosts) - undead_type field for variants
  • Bandit - Human enemy with "aggressive" and "wandering" states
  • GroundGame (game boars, mountain goats, rams) - animal_type field for variants
  • FlyingCreature (geese) - Config-driven with height, state fields

Required Methods

Each creature class must implement:

void update_creature(Creature@ creature) {
    // Movement logic
    // Attack logic
    // Sound management
}

void clear_creatures() {
    for (uint i = 0; i < creatures.length(); i++) {
        if (creatures[i].sound_handle != -1) {
            p.destroy_sound(creatures[i].sound_handle);
            creatures[i].sound_handle = -1;
        }
    }
    creatures.resize(0);
}

Update Loop Pattern

void update_creatures() {
    for (uint i = 0; i < creatures.length(); i++) {
        update_creature(creatures[i]);
    }
}

Sound Handle Lifecycle Management

Proper sound handle cleanup prevents memory leaks and ensures clean audio.

Initialization

class Creature {
    int sound_handle = -1;  // Always initialize to -1 (invalid handle)

    void play_sound() {
        if (sound_handle != -1) {
            p.destroy_sound(sound_handle);  // Clean up existing sound first
        }
        sound_handle = p.play_1d("sounds/creature.ogg", position, true);
    }
}

Cleanup on Death/Destroy

void destroy_creature(Creature@ creature) {
    if (creature.sound_handle != -1) {
        p.destroy_sound(creature.sound_handle);
        creature.sound_handle = -1;
    }
}

Clear All Pattern

void clear_creatures() {
    for (uint i = 0; i < creatures.length(); i++) {
        if (creatures[i].sound_handle != -1) {
            p.destroy_sound(creatures[i].sound_handle);
            creatures[i].sound_handle = -1;
        }
    }
    creatures.resize(0);
}

Removal During Update Loop

When removing creatures during update iteration:

void update_creatures() {
    for (uint i = 0; i < creatures.length(); i++) {
        update_creature(creatures[i]);

        if (creatures[i].health <= 0) {
            if (creatures[i].sound_handle != -1) {
                p.destroy_sound(creatures[i].sound_handle);
                creatures[i].sound_handle = -1;
            }
            creatures.remove_at(i);
            i--;  // Critical: decrement to skip the shifted element
        }
    }
}

Key Rules

  1. Always initialize sound_handle to -1
  2. Check before destroying: Always check sound_handle != -1 before calling p.destroy_sound()
  3. Reset after destroy: Set sound_handle = -1 after destroying
  4. Clean before allocate: If a sound is already playing, destroy it before creating a new one
  5. Iterate carefully: When removing elements during iteration, decrement the index after remove_at(i)

Death Sound Pattern

void play_creature_death_sound(string sound_path, int player_x, int creature_x, int volume_step) {
    int distance = abs(player_x - creature_x);
    if (distance <= volume_step) {
        p.play_stationary(sound_path, false);
    } else {
        // Too far to hear
    }
}

File Organization Patterns

Module Structure

Each major system is organized into:

  1. Orchestrator file (in src/): Includes all sub-modules
  2. Sub-module files (in subdirectories): Focused functionality

Example:

src/
  inventory_menus.nvgt (orchestrator, ~45 lines)
  menus/
    menu_utils.nvgt (helpers)
    inventory_core.nvgt (personal inventory)
    storage_menu.nvgt (storage interactions)
    equipment_menu.nvgt (equipment)
    action_menu.nvgt (context actions)
    character_info.nvgt (stats display)
    base_info.nvgt (base status)
    altar_menu.nvgt (sacrifice)

Include Order

Orchestrator files include dependencies in order:

// Utilities first (others depend on these)
#include "base/module_utils.nvgt"

// Independent modules next
#include "base/module_a.nvgt"
#include "base/module_b.nvgt"

// Dependent modules last
#include "base/module_c.nvgt"  // depends on A and B

Menu System Patterns

Menu Loop Structure

void run_menu() {
    speak_with_history("Menu name.", true);

    int selection = 0;
    string[] options = {"Option 1", "Option 2"};

    while(true) {
        wait(5);
        menu_background_tick();  // Always call this first

        if (key_pressed(KEY_ESCAPE)) {
            speak_with_history("Closed.", true);
            break;
        }

        if (key_pressed(KEY_DOWN)) {
            selection++;
            if (selection >= options.length()) selection = 0;
            speak_with_history(options[selection], true);
        }

        if (key_pressed(KEY_UP)) {
            selection--;
            if (selection < 0) selection = options.length() - 1;
            speak_with_history(options[selection], true);
        }

        if (key_pressed(KEY_RETURN)) {
            // Handle selection
            break;  // or rebuild options
        }
    }
}

Menu Background Tick

All menus must call menu_background_tick() each iteration to:

  • Update time
  • Update environment
  • Process game events (fires, enemies, blessings)
  • Check for fire damage
  • Handle healing
  • Check for death

This ensures the game continues running while menus are open.