Implement i18n audit/localization cleanup and sync libstorm submodule

This commit is contained in:
Storm Dragon
2026-02-24 23:14:40 -05:00
parent b77b895685
commit c5d26d5edd
68 changed files with 9169 additions and 853 deletions
+71
View File
@@ -0,0 +1,71 @@
# Localization Guide
Draugnorak uses key-based localization files in `lang/`.
## File Layout
- `lang/en.ini`: source English catalog used at runtime fallback.
- `lang/en.template.ini`: copy this to start a new language.
- `lang/<code>.ini`: translator file (examples: `es.ini`, `pt-BR.ini`).
Language preference is saved to `draugnorak.dat` in the appdata config directory.
## Create a New Translation
1. Copy `lang/en.template.ini` to `lang/<code>.ini`.
2. Edit `[meta]`:
- `code`
- `name`
- `native_name`
3. Translate only values (right side of `=`). Never change keys.
4. Keep placeholders exactly as written, for example `{arg1}` or `{language}`.
## Placeholder Rules
- You may move placeholders in the sentence.
- Do not rename or remove placeholders.
- Example:
- English: `Language set to {language}.`
- Spanish: `Idioma establecido en {language}.`
## Validate Translation Files
Run:
```bash
python3 scripts/validate_i18n_catalog.py
```
This checks:
- missing keys
- extra keys
- placeholder mismatches
## Audit Untranslated Strings
Run:
```bash
python3 scripts/audit_untranslated_strings.py
```
This checks likely user-facing string literals that are not passed through translation wrappers.
Use `scripts/i18n_audit_allowlist.txt` for intentional exceptions. Keep this list small and explain each exception with a comment.
## Regenerate English Catalog
When text changes in code, regenerate English source/template catalogs:
```bash
python3 scripts/generate_i18n_catalog.py
```
This updates:
- `lang/en.ini`
- `lang/en.template.ini`
## Runtime Behavior
- Missing key in selected language: falls back to English.
- Missing key in English catalog: key text is spoken/displayed.
+278
View File
@@ -0,0 +1,278 @@
# 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
```nvgt
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
```nvgt
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:
```nvgt
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
```nvgt
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
```nvgt
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
```nvgt
void destroy_creature(Creature@ creature) {
if (creature.sound_handle != -1) {
p.destroy_sound(creature.sound_handle);
creature.sound_handle = -1;
}
}
```
### Clear All Pattern
```nvgt
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:
```nvgt
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
```nvgt
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:
```nvgt
// 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
```nvgt
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.
+139
View File
@@ -0,0 +1,139 @@
# Rune System
Runes are permanent enchantments that can be engraved onto equipment to grant bonuses. Each piece of equipment can only have one rune.
## How It Works
1. **Unlock**: Defeat a boss to unlock a rune (e.g., unicorn boss unlocks Rune of Swiftness)
2. **Craft**: Go to Crafting > Runes menu (only visible after unlocking at least one rune)
3. **Requirements**: Knife (tool, not consumed), 1 Clay, 1 Favor, must be in base area
4. **Result**: Equipment becomes "Runed {Item} of {Effect}" (e.g., "Runed Skin Pants of Quickness")
## Current Runes
### Rune of Swiftness
- **Unlocked by**: Defeating the unicorn boss
- **Effect name**: "of Quickness"
- **Walk speed bonus**: 20ms per runed item (moccasins give 40ms)
- **Gathering bonus**: 5% faster per runed item (capped at 50% total)
- **Stacking**: All bonuses stack (moccasins + all runed items + speed blessing)
## Files Overview
| File | Purpose |
|------|---------|
| `src/runes/rune_data.nvgt` | Core data structures, constants, unlock tracking, dictionary storage |
| `src/runes/rune_effects.nvgt` | Speed bonus calculations for walking and gathering |
| `src/crafting/craft_runes.nvgt` | Rune engraving crafting menu |
## Adding a New Rune
### Step 1: Add Constants (rune_data.nvgt)
```nvgt
const int RUNE_NEWRUNE = 1; // Next available ID
bool rune_newrune_unlocked = false;
```
### Step 2: Update Helper Functions (rune_data.nvgt)
```nvgt
// In get_rune_name()
if (rune_type == RUNE_NEWRUNE) return "Rune of NewEffect";
// In get_rune_effect_name()
if (rune_type == RUNE_NEWRUNE) return "NewEffect";
// In any_rune_unlocked()
return rune_swiftness_unlocked || rune_newrune_unlocked;
// In is_rune_unlocked()
if (rune_type == RUNE_NEWRUNE) return rune_newrune_unlocked;
// In reset_rune_data()
rune_newrune_unlocked = false;
```
### Step 3: Add Effect Calculations (rune_effects.nvgt)
For a combat rune example:
```nvgt
int get_total_rune_damage_bonus() {
int bonus = 0;
if (equipped_weapon_rune == RUNE_NEWRUNE) bonus += RUNE_NEWRUNE_DAMAGE_BONUS;
return bonus;
}
```
### Step 4: Add Crafting Option (craft_runes.nvgt)
```nvgt
// In run_runes_menu(), add to the menu building:
if (rune_newrune_unlocked) {
rune_options.insert_last("Rune of NewEffect (1 Clay, 1 Favor) [Requires Knife]");
rune_types.insert_last(RUNE_NEWRUNE);
}
```
### Step 5: Add Save/Load (save_system.nvgt)
```nvgt
// In save_game_state():
saveData.set("rune_newrune_unlocked", rune_newrune_unlocked);
// In load_game_state():
rune_newrune_unlocked = get_bool(saveData, "rune_newrune_unlocked", false);
```
### Step 6: Set Unlock Condition
In the boss victory function or quest reward:
```nvgt
rune_newrune_unlocked = true;
```
### Step 7: Update Equipment Menu (if needed)
If the new rune needs special display in equipment_menu.nvgt:
```nvgt
// In the runed items loop, add check for new rune type:
int count = get_runed_item_count(equip_type, RUNE_NEWRUNE);
if (count > 0) {
// Add to menu...
}
```
## Data Storage
### Rune Unlocks
Simple boolean variables per rune type, saved individually.
### Runed Items
Uses a dictionary with key format `"equip_type:rune_type"` and count as value.
- Example: `"5:0"` = Skin Pants (EQUIP_PANTS=5) with Swiftness (RUNE_SWIFTNESS=0)
- Saved as comma-separated `key=count` pairs
### Equipped Runes
Tracked per equipment slot:
- `equipped_head_rune`
- `equipped_torso_rune`
- `equipped_arms_rune`
- `equipped_hands_rune`
- `equipped_legs_rune`
- `equipped_feet_rune`
- `equipped_weapon_rune`
## Equipment Types That Can Be Runed
All current equipment (defined in `get_runeable_equipment_types()`):
- Weapons: Spear, Axe, Sling, Bow
- Clothing: Skin Hat, Skin Gloves, Skin Pants, Skin Tunic, Moccasins, Skin Pouch, Backpack
## Constants Reference
| Constant | Value | Description |
|----------|-------|-------------|
| `RUNE_NONE` | -1 | No rune applied |
| `RUNE_SWIFTNESS` | 0 | Swiftness rune type |
| `RUNE_SWIFTNESS_SPEED_BONUS` | 20 | Walk speed reduction in ms |
| `RUNE_SWIFTNESS_GATHER_BONUS` | 5 | Gathering speed reduction % |