Implement i18n audit/localization cleanup and sync libstorm submodule
This commit is contained in:
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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 % |
|
||||
Reference in New Issue
Block a user