7.0 KiB
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 positionBandit@ get_bandit_at(int pos)- Find bandit at positionGroundGame@ get_ground_game_at(int pos)- Find ground game animal at positionFlyingCreature@ get_flying_creature_at(int pos)- Find flying creature at positionWorldFire@ get_fire_at(int pos)- Find fire at positionWorldSnare@ get_snare_at(int pos)- Find snare at positionWorldStream@ get_stream_at(int pos)- Find stream at positionWorldHerbGarden@ get_herb_garden_at_base()- Special case for base buildings
Usage Notes
- Always returns
nullif no object found at position - Use handle return type
@ClassNamefor 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_typefield for variantsBandit- Human enemy with "aggressive" and "wandering" statesGroundGame(game boars, mountain goats, rams) -animal_typefield for variantsFlyingCreature(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
- Always initialize sound_handle to -1
- Check before destroying: Always check
sound_handle != -1before callingp.destroy_sound() - Reset after destroy: Set
sound_handle = -1after destroying - Clean before allocate: If a sound is already playing, destroy it before creating a new one
- 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:
- Orchestrator file (in
src/): Includes all sub-modules - 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.