From c5d26d5edd06523d8fabfd506bc618d042a351c6 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Tue, 24 Feb 2026 23:14:40 -0500 Subject: [PATCH] Implement i18n audit/localization cleanup and sync libstorm submodule --- .gitignore | 3 + bugs.txt | 5 + docs/localization.md | 71 + docs/patterns.md | 278 +++ docs/rune_system.md | 139 ++ draugnorak.nvgt | 197 ++- excluded_sounds.nvgt | 82 +- files/instructions.md | 4 + lang/af.ini | 802 +++++++++ lang/en.ini | 1531 +++++++++++++++++ lang/en.template.ini | 1531 +++++++++++++++++ lang/es.ini | 802 +++++++++ libstorm-nvgt | 2 +- minigame.nvgt | 77 + package.sh | 223 +++ scripts/audit_untranslated_strings.py | 401 +++++ scripts/generate_i18n_catalog.py | 660 +++++++ scripts/i18n_audit_allowlist.txt | 2 + scripts/validate_i18n_catalog.py | 146 ++ skills/nvgt-engine-dev/SKILL.md | 66 + skills/nvgt-engine-dev/agents/openai.yaml | 4 + .../references/api-verification-workflow.md | 44 + .../references/common-playbooks.md | 43 + .../references/nvgt-coding-patterns.md | 43 + .../references/project-profile.md | 41 + .../references/regression-checklist.md | 35 + skills/nvgt-engine-dev/references/repo-map.md | 73 + .../scripts/nvgt_api_report.py | 96 ++ skills/nvgt-engine-dev/scripts/nvgt_lookup.py | 120 ++ sounds/enemies/draugrhaugr1.ogg | 3 + sounds/terrain/draugrhaugr_step.ogg | 3 + src/audio_utils.nvgt | 52 +- src/bosses/adventure_system.nvgt | 2 + src/bosses/bandit_hideout.nvgt | 14 +- src/bosses/unicorn/unicorn_boss.nvgt | 16 +- src/crafting/craft_barricade.nvgt | 34 +- src/crafting/craft_buildings.nvgt | 32 +- src/crafting/craft_clothing.nvgt | 72 +- src/crafting/craft_materials.nvgt | 71 +- src/crafting/craft_runes.nvgt | 60 +- src/crafting/craft_tools.nvgt | 88 +- src/crafting/craft_weapons.nvgt | 42 +- src/crafting/crafting_core.nvgt | 135 +- src/environment.nvgt | 63 +- src/fishing.nvgt | 2 +- src/fylgja_system.nvgt | 7 +- src/i18n.nvgt | 770 +++++++++ src/inventory_items.nvgt | 67 +- src/inventory_menus.nvgt | 2 +- src/item_registry.nvgt | 34 +- src/menus/action_menu.nvgt | 1 + src/menus/base_info.nvgt | 1 + src/menus/character_info.nvgt | 124 +- src/menus/equipment_menu.nvgt | 56 +- src/menus/inventory_core.nvgt | 41 +- src/menus/storage_menu.nvgt | 74 +- src/pet_system.nvgt | 16 +- src/quest_system.nvgt | 2 +- src/quests/bat_invasion_game.nvgt | 11 +- src/quests/catch_the_boomerang_game.nvgt | 11 +- src/quests/enchanted_melody_game.nvgt | 11 +- src/quests/escape_from_hel_game.nvgt | 13 +- src/quests/skeletal_bard_game.nvgt | 11 +- src/save_system.nvgt | 252 +-- src/text_reader_aliases.nvgt | 6 +- src/world/world_drops.nvgt | 24 +- todo.txt | 72 + translate.md | 206 +++ 68 files changed, 9169 insertions(+), 853 deletions(-) create mode 100644 bugs.txt create mode 100644 docs/localization.md create mode 100644 docs/patterns.md create mode 100644 docs/rune_system.md mode change 100755 => 100644 draugnorak.nvgt create mode 100644 lang/af.ini create mode 100644 lang/en.ini create mode 100644 lang/en.template.ini create mode 100644 lang/es.ini create mode 100644 minigame.nvgt create mode 100755 package.sh create mode 100755 scripts/audit_untranslated_strings.py create mode 100644 scripts/generate_i18n_catalog.py create mode 100644 scripts/i18n_audit_allowlist.txt create mode 100644 scripts/validate_i18n_catalog.py create mode 100644 skills/nvgt-engine-dev/SKILL.md create mode 100644 skills/nvgt-engine-dev/agents/openai.yaml create mode 100644 skills/nvgt-engine-dev/references/api-verification-workflow.md create mode 100644 skills/nvgt-engine-dev/references/common-playbooks.md create mode 100644 skills/nvgt-engine-dev/references/nvgt-coding-patterns.md create mode 100644 skills/nvgt-engine-dev/references/project-profile.md create mode 100644 skills/nvgt-engine-dev/references/regression-checklist.md create mode 100644 skills/nvgt-engine-dev/references/repo-map.md create mode 100755 skills/nvgt-engine-dev/scripts/nvgt_api_report.py create mode 100755 skills/nvgt-engine-dev/scripts/nvgt_lookup.py create mode 100644 sounds/enemies/draugrhaugr1.ogg create mode 100644 sounds/terrain/draugrhaugr_step.ogg create mode 100644 src/i18n.nvgt create mode 100644 todo.txt create mode 100644 translate.md diff --git a/.gitignore b/.gitignore index 236e223..52476f7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ nvgt +*.apk +*.idsig +*.keystore *.zip lib/ lib_mac/ diff --git a/bugs.txt b/bugs.txt new file mode 100644 index 0000000..b98b20f --- /dev/null +++ b/bugs.txt @@ -0,0 +1,5 @@ +Sometimes with shif held down to search the character stops searching. +Sometimes when arrows are held down the character stops walking. +pressing the button again fixes the issue, but I wonder why it happens. +Arrows cannot hit trees. This is inconsistant with sling behavior. +The bow is not available in the unicorn adventure. diff --git a/docs/localization.md b/docs/localization.md new file mode 100644 index 0000000..ea2591d --- /dev/null +++ b/docs/localization.md @@ -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/.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/.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. diff --git a/docs/patterns.md b/docs/patterns.md new file mode 100644 index 0000000..1238d3e --- /dev/null +++ b/docs/patterns.md @@ -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. diff --git a/docs/rune_system.md b/docs/rune_system.md new file mode 100644 index 0000000..7ac350b --- /dev/null +++ b/docs/rune_system.md @@ -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 % | diff --git a/draugnorak.nvgt b/draugnorak.nvgt old mode 100755 new mode 100644 index cc8212a..77fb196 --- a/draugnorak.nvgt +++ b/draugnorak.nvgt @@ -7,6 +7,7 @@ sound_pool p(300); #include "src/constants.nvgt" +#include "src/i18n.nvgt" #include "src/player.nvgt" #include "src/creature_base.nvgt" #include "src/creature_death.nvgt" @@ -69,13 +70,13 @@ int run_main_menu() { speak_with_history("Draugnorak. Main menu.", true); int selection = 0; - string load_label = has_save_game() ? "Load Game" : "Load Game (no saves found)"; + string load_label = has_save_game() ? i18n_text("Load Game") : i18n_text("Load Game (no saves found)"); string[] options; string[] entry_paths; int[] entry_types; // 0 = action, 1 = document int[] action_ids; - options.insert_last("New Game"); + options.insert_last(i18n_text("New Game")); entry_paths.insert_last(""); entry_types.insert_last(0); action_ids.insert_last(MAIN_MENU_NEW_GAME); @@ -85,7 +86,7 @@ int run_main_menu() { entry_types.insert_last(0); action_ids.insert_last(MAIN_MENU_LOAD_GAME); - options.insert_last("Learn Sounds"); + options.insert_last(i18n_text("Learn Sounds")); entry_paths.insert_last(""); entry_types.insert_last(0); action_ids.insert_last(MAIN_MENU_LEARN_SOUNDS); @@ -101,7 +102,7 @@ int run_main_menu() { action_ids.insert_last(-1); } - options.insert_last("Exit"); + options.insert_last(i18n_text("Exit")); entry_paths.insert_last(""); entry_types.insert_last(0); action_ids.insert_last(MAIN_MENU_EXIT); @@ -109,7 +110,7 @@ int run_main_menu() { speak_with_history(options[selection], true); - while(true) { + while (true) { wait(5); mainMenuMusic.loop(); handle_global_volume_keys(); @@ -117,14 +118,16 @@ int run_main_menu() { if (key_pressed(KEY_DOWN)) { play_menu_move_sound(); selection++; - if (selection >= options.length()) selection = 0; + if (selection >= options.length()) + selection = 0; speak_with_history(options[selection], true); } if (key_pressed(KEY_UP)) { play_menu_move_sound(); selection--; - if (selection < 0) selection = options.length() - 1; + if (selection < 0) + selection = options.length() - 1; speak_with_history(options[selection], true); } @@ -150,15 +153,18 @@ int run_main_menu() { return exit_action; } -void run_game() -{ +void run_game() { // Configure sound pool for better spatial audio - p.volume_step = AUDIO_VOLUME_STEP / float(AUDIO_TILE_SCALE); // Default falloff in audio units - p.pan_step = AUDIO_PAN_STEP; // Panning strength for scaled tile distances + p.volume_step = AUDIO_VOLUME_STEP / float(AUDIO_TILE_SCALE); // Default falloff in audio units + p.pan_step = AUDIO_PAN_STEP; // Panning strength for scaled tile distances init_master_volume(); show_window("Draugnorak"); ui_set_default_window_title("Draugnorak"); + i18n_init(); + string localizedWindowTitle = tr("system.ui.window_title"); + show_window(localizedWindowTitle); + ui_set_default_window_title(localizedWindowTitle); init_flying_creature_configs(); init_item_registry(); init_pet_sounds(); @@ -190,7 +196,8 @@ void run_game() game_started = true; } else { string message = last_save_error; - if (message == "") message = "Unable to load save."; + if (message == "") + message = "Unable to load save."; ui_info_box("Draugnorak", "Load Game", message); } } else if (selection == MAIN_MENU_LEARN_SOUNDS) { @@ -201,11 +208,11 @@ void run_game() } } - while (game_started) { - wait(5); - handle_global_volume_keys(); - - if (return_to_main_menu_requested) { + while (game_started) { + wait(5); + handle_global_volume_keys(); + + if (return_to_main_menu_requested) { game_paused = false; menuBackgroundUpdatesEnabled = true; p.resume_all(); @@ -213,10 +220,9 @@ void run_game() return_to_main_menu_requested = false; break; } - + // Pause toggle - if(key_pressed(KEY_BACK)) - { + if (key_pressed(KEY_BACK)) { game_paused = !game_paused; if (game_paused) { p.pause_all(); @@ -229,7 +235,7 @@ void run_game() } continue; } - + // Skip all game logic while paused if (game_paused) { continue; @@ -237,15 +243,14 @@ void run_game() check_pet_call_key(); - if(key_pressed(KEY_ESCAPE)) - { - int really_exit = ui_question("", "Really exit?"); - if (really_exit == 1) { - return_to_main_menu_requested = true; - continue; + if (key_pressed(KEY_ESCAPE)) { + int really_exit = ui_question("", "Really exit?"); + if (really_exit == 1) { + return_to_main_menu_requested = true; + continue; + } } - } - + // Time & Environment updates update_time(); update_crossfade(); @@ -263,27 +268,28 @@ void run_game() update_pets(); update_blessings(); update_notifications(); - + // Fire damage check (only if not jumping) - WorldFire@ fire_on_tile = get_fire_at(x); + WorldFire @fire_on_tile = get_fire_at(x); if (fire_on_tile != null && !jumping && fire_damage_timer.elapsed > 1000) { player_health--; fire_damage_timer.restart(); speak_with_history("Burning! " + player_health + " health remaining.", true); } - + // Healing in base area if (x <= BASE_END && player_health < max_health) { - WorldHerbGarden@ herb_garden = get_herb_garden_at_base(); - int heal_interval = (herb_garden != null) ? 30000 : 150000; // 30 seconds with garden, 2.5 minutes without - + WorldHerbGarden @herb_garden = get_herb_garden_at_base(); + int heal_interval = + (herb_garden != null) ? 30000 : 150000; // 30 seconds with garden, 2.5 minutes without + if (healing_timer.elapsed > heal_interval) { player_health++; healing_timer.restart(); speak_with_history(player_health + " health.", true); } } - + // Death check if (player_health <= 0) { if (!try_consume_heal_scroll()) { @@ -293,12 +299,12 @@ void run_game() continue; } } - + if (fylgjaCharging) { update_fylgja_charge(); continue; } - + // Inventory & Actions check_inventory_keys(x); check_action_menu(x); @@ -312,12 +318,12 @@ void run_game() check_time_input(); check_notification_keys(); check_speech_history_keys(); - + // Health Key if (key_pressed(KEY_H)) { speak_with_history(get_health_report(), true); } - + // Coordinates Key if (key_pressed(KEY_X)) { string direction_label = (facing == 1) ? "east" : "west"; @@ -327,19 +333,19 @@ void run_game() } speak_with_history(direction_label + ", x " + x + ", y " + y + ", terrain " + terrain, true); } - + // Base Info Key (base only) if (key_pressed(KEY_B)) { run_base_info_menu(); } - + // Climbing and Falling Updates update_climbing(); update_falling(); update_rope_climbing(); check_rope_climb_fall(); update_mountains(); - + // Down arrow to climb down from tree or start rope climb down if (key_pressed(KEY_DOWN)) { // Check for pending rope climb (going down) @@ -352,16 +358,16 @@ void run_game() } // Tree climbing down else { - Tree@ tree = get_tree_at(x); - if (tree != null && !tree.is_chopped && y > 0 && !jumping && !climbing && !falling && !rope_climbing) { + Tree @tree = get_tree_at(x); + if (tree != null && !tree.is_chopped && y > 0 && !jumping && !climbing && !falling && + !rope_climbing) { climb_down_tree(); } } } - + // Jumping Logic - if(key_pressed(KEY_UP)) - { + if (key_pressed(KEY_UP)) { // Check for pending rope climb (going up) if (pending_rope_climb_x != -1 && !rope_climbing && !climbing && !falling && !jumping) { int elevation_change = pending_rope_climb_elevation - y; @@ -369,14 +375,12 @@ void run_game() start_rope_climb(true, pending_rope_climb_x, pending_rope_climb_elevation); pending_rope_climb_x = -1; } - } - else if(!jumping && !climbing && !falling && !rope_climbing) - { + } else if (!jumping && !climbing && !falling && !rope_climbing) { // Get ground elevation at current position int ground_elevation = get_mountain_elevation_at(x); - + // Check if on tree tile - Tree@ tree = get_tree_at(x); + Tree @tree = get_tree_at(x); if (tree != null && !tree.is_chopped && y == ground_elevation) { // Start climbing the tree start_climbing_tree(x); @@ -388,9 +392,8 @@ void run_game() } } } - - if(jumping && jumptimer.elapsed > 850) - { + + if (jumping && jumptimer.elapsed > 850) { jumping = false; // Reset y to ground elevation after jump y = get_mountain_elevation_at(x); @@ -398,17 +401,17 @@ void run_game() // Check for snare on landing? check_snare_collision(x); } - + // Set y to 3 above ground during jump - if(jumping && y == get_mountain_elevation_at(x)) { + if (jumping && y == get_mountain_elevation_at(x)) { y = get_mountain_elevation_at(x) + 3; } - + movetime = jumping ? jump_speed : walk_speed; - + bool left_active = key_down(KEY_LEFT); bool right_active = key_down(KEY_RIGHT); - + // Movement Logic if (key_pressed(KEY_LEFT) && facing != 0 && !climbing && !falling && !rope_climbing) { facing = 0; @@ -424,22 +427,21 @@ void run_game() // Cancel pending rope climb when changing direction pending_rope_climb_x = -1; } - - if(walktimer.elapsed > movetime) - { + + if (walktimer.elapsed > movetime) { int old_x = x; - + // Check if trying to move left/right while in a tree. - Tree@ current_tree = get_tree_at(x); + Tree @current_tree = get_tree_at(x); int ground_elevation = get_mountain_elevation_at(x); - if((left_active || right_active) && y > ground_elevation && !jumping && !falling && !rope_climbing && current_tree !is null) { + if ((left_active || right_active) && y > ground_elevation && !jumping && !falling && !rope_climbing && + current_tree !is null) { // Fall out of tree climbing = false; start_falling(); } - - if(left_active && x > 0 && !climbing && !falling && !rope_climbing) - { + + if (left_active && x > 0 && !climbing && !falling && !rope_climbing) { facing = 0; int target_x = x - 1; // Check mountain movement and deep water @@ -451,14 +453,12 @@ void run_game() y = new_elevation; } walktimer.restart(); - if(!jumping) { + if (!jumping) { play_footstep(x, BASE_END, GRASS_END); check_snare_collision(x); } } - } - else if(right_active && x < MAP_SIZE - 1 && !climbing && !falling && !rope_climbing) - { + } else if (right_active && x < MAP_SIZE - 1 && !climbing && !falling && !rope_climbing) { facing = 1; int target_x = x + 1; // Check mountain movement and deep water @@ -470,22 +470,22 @@ void run_game() y = new_elevation; } walktimer.restart(); - if(!jumping) { + if (!jumping) { play_footstep(x, BASE_END, GRASS_END); check_snare_collision(x); } } } } - + // Reset search timer if shift is used with other keys - if((key_down(KEY_LSHIFT) || key_down(KEY_RSHIFT))) { - if(key_pressed(KEY_COMMA) || key_pressed(KEY_PERIOD)) { + if ((key_down(KEY_LSHIFT) || key_down(KEY_RSHIFT))) { + if (key_pressed(KEY_COMMA) || key_pressed(KEY_PERIOD)) { searching = false; search_delay_timer.restart(); } } - + // Searching Logic bool shift_down = (key_down(KEY_LSHIFT) || key_down(KEY_RSHIFT)); bool search_key_down = shift_down || key_down(KEY_SLASH) || key_down(KEY_Z); @@ -494,25 +494,23 @@ void run_game() } // Apply rune gathering bonus to search time int search_time = apply_rune_gather_bonus(2000); - if (search_key_down && search_timer.elapsed > search_time && !searching) - { + if (search_key_down && search_timer.elapsed > search_time && !searching) { searching = true; search_delay_timer.restart(); } - + // Complete search after delay - if(searching && search_delay_timer.elapsed >= 1000) - { + if (searching && search_delay_timer.elapsed >= 1000) { searching = false; search_timer.restart(); perform_search(x); } - + update_fishing(); update_bow_shot(); - + bool ctrl_down = (key_down(KEY_LCTRL) || key_down(KEY_RCTRL)); - + // Bow draw detection if (bow_equipped) { if (ctrl_down && !bow_drawing) { @@ -524,7 +522,7 @@ void run_game() speak_ammo_blocked("No arrows."); } } - + if (bow_drawing && !ctrl_down) { release_bow_attack(x); bow_drawing = false; @@ -533,7 +531,7 @@ void run_game() if (!bow_equipped && bow_drawing) { bow_drawing = false; } - + // Sling charge detection if (!bow_equipped && sling_equipped && ctrl_down && !sling_charging) { if (get_personal_count(ITEM_STONES) > 0) { @@ -545,12 +543,12 @@ void run_game() speak_ammo_blocked("No stones."); } } - + // Update sling charge state while holding if (sling_charging && ctrl_down) { update_sling_charge(); } - + // Sling release detection if (sling_charging && !ctrl_down) { release_sling_attack(x); @@ -560,31 +558,32 @@ void run_game() sling_sound_handle = -1; } } - + // Non-sling weapon attacks (existing pattern) if (!bow_equipped && !bow_drawing && !sling_equipped && !sling_charging) { int attack_cooldown = 1000; - if (spear_equipped) attack_cooldown = 800; - if (axe_equipped) attack_cooldown = 1600; - + if (spear_equipped) + attack_cooldown = 800; + if (axe_equipped) + attack_cooldown = 1600; + if (!fishing_pole_equipped && ctrl_down && attack_timer.elapsed > attack_cooldown) { attack_timer.restart(); perform_attack(x); } } - + // Audio Listener Update p.update_listener_1d(x); } } } -void main() -{ +void main() { try { run_game(); } catch { log_unhandled_exception("main"); - ui_info_box("Draugnorak", "Unhandled exception", "A crash log was written to crash.log."); + ui_info_box("Draugnorak", "Unhandled exception", "A crash log was written to your save data folder."); } } diff --git a/excluded_sounds.nvgt b/excluded_sounds.nvgt index c2339fe..e00e810 100644 --- a/excluded_sounds.nvgt +++ b/excluded_sounds.nvgt @@ -22,66 +22,28 @@ void configure_project_learn_sounds() { learn_sounds_add_description( "sounds/actions/bad_cast.ogg", - "Your cast missed the water and landed in nearby foliage where it is unlikely to attract fish." - ); + "Your cast missed the water and landed in nearby foliage where it is unlikely to attract fish."); learn_sounds_add_description( "sounds/actions/cast_strength.ogg", - "When casting release control when over water. When catching release when sound is over player." - ); - learn_sounds_add_description( - "sounds/enemies/enter_range.ogg", - "An enemy is in range of your currently wielded weapon." - ); - learn_sounds_add_description( - "sounds/enemies/exit_range.ogg", - "An enemy is no longer in range of your currently wielded weapon." - ); - learn_sounds_add_description( - "sounds/actions/falling.ogg", - "Lowers in pitch as the fall progresses." - ); - learn_sounds_add_description( - "sounds/enemies/invasion.ogg", - "The war drums pound! Prepare for combat!" - ); - learn_sounds_add_description( - "sounds/items/miscellaneous.ogg", - "You picked up an item for which there is no specific sound." - ); - learn_sounds_add_description( - "sounds/enemies/goblin1.ogg", - "Inhabits mountainous regions." - ); - learn_sounds_add_description( - "sounds/enemies/undead_resident1.ogg", - "Not quite a zombie nor a vampyr. These undead remember they used to have a home, and maybe there's food there..." - ); - learn_sounds_add_description( - "sounds/enemies/wight1.ogg", - "More powerful undead. They do not lose track of you when you are in the base." - ); - learn_sounds_add_description( - "sounds/enemies/vampyr1.ogg", - "Ever hungry, they feed upon your residents." - ); - learn_sounds_add_description( - "sounds/enemies/vampyr2.ogg", - "Ever hungry, they feed upon your residents." - ); - learn_sounds_add_description( - "sounds/enemies/vampyr3.ogg", - "Ever hungry, they feed upon your residents." - ); - learn_sounds_add_description( - "sounds/enemies/vampyr4.ogg", - "Ever hungry, they feed upon your residents." - ); - learn_sounds_add_description( - "sounds/enemies/yeti1.ogg", - "Inhabits snowy regions." - ); - learn_sounds_add_description( - "sounds/enemies/zombie1.ogg", - "Basic undead." - ); + "When casting release control when over water. When catching release when sound is over player."); + learn_sounds_add_description("sounds/enemies/enter_range.ogg", + "An enemy is in range of your currently wielded weapon."); + learn_sounds_add_description("sounds/enemies/exit_range.ogg", + "An enemy is no longer in range of your currently wielded weapon."); + learn_sounds_add_description("sounds/actions/falling.ogg", "Lowers in pitch as the fall progresses."); + learn_sounds_add_description("sounds/enemies/invasion.ogg", "The war drums pound! Prepare for combat!"); + learn_sounds_add_description("sounds/items/miscellaneous.ogg", + "You picked up an item for which there is no specific sound."); + learn_sounds_add_description("sounds/enemies/goblin1.ogg", "Inhabits mountainous regions."); + learn_sounds_add_description("sounds/enemies/undead_resident1.ogg", + "Not quite a zombie nor a vampyr. These undead remember they used to have a home, and " + "maybe there's food there..."); + learn_sounds_add_description("sounds/enemies/wight1.ogg", + "More powerful undead. They do not lose track of you when you are in the base."); + learn_sounds_add_description("sounds/enemies/vampyr1.ogg", "Ever hungry, they feed upon your residents."); + learn_sounds_add_description("sounds/enemies/vampyr2.ogg", "Ever hungry, they feed upon your residents."); + learn_sounds_add_description("sounds/enemies/vampyr3.ogg", "Ever hungry, they feed upon your residents."); + learn_sounds_add_description("sounds/enemies/vampyr4.ogg", "Ever hungry, they feed upon your residents."); + learn_sounds_add_description("sounds/enemies/yeti1.ogg", "Inhabits snowy regions."); + learn_sounds_add_description("sounds/enemies/zombie1.ogg", "Basic undead."); } diff --git a/files/instructions.md b/files/instructions.md index e518b97..540dafa 100644 --- a/files/instructions.md +++ b/files/instructions.md @@ -11,6 +11,10 @@ Explore gather craft defend your base through the day and night cycle chmod +x draugnorak ``` +### Android Note +- Android builds require an external keyboard (USB or Bluetooth) +- Touch controls are not implemented because too many game controls to do this in a good way for playability + ## Quick Start - Move with Left or Right - Jump with Up diff --git a/lang/af.ini b/lang/af.ini new file mode 100644 index 0000000..0fa05c1 --- /dev/null +++ b/lang/af.ini @@ -0,0 +1,802 @@ +; Draugnorak localization catalog +; Copy this file to lang/.ini and translate only the right-hand values. +; Keep keys unchanged. Use placeholders like {arg1}, {count}, {language} exactly as written. + +[meta] +code=af +name=Afrikaans +native_name=Afrikaans + +[system] + +[messages] +language.select_prompt=Kies jou taal. +language.selected=Taal gestel op {language}. +language.english_label=English (en) +ui.window_title=Draugnorak +sex.male=Manlik +sex.female=Vroulik +new_character.choose_sex=Kies jou geslag. +new_character.enter_name=Voer jou naam in of druk Enter vir 'n ewekansige naam. +new_character.save_exists_overwrite=Stoorlêer gevind vir {name}. Overskryf? +load_game.option_with_metadata={name}, {sex}, dag {day} +load_game.delete_confirm_base=Is jy seker jy wil die karakter {name} uitvee? +load_game.delete_confirm_with_metadata=Is jy seker jy wil die karakter {name} geslag {sex} dae {day} uitvee? +load_game.delete_save_heading=Vee stoorlêer uit +load_game.delete_save_failed=Kon nie stoorlêer uitvee nie. +quick_slot.no_item_bound=Geen item aan gleuf {slot} gekoppel nie. +menu.closed=Gesluit. +menu.canceled=Gekanselleer. +menu.no_options=Geen opsies nie. +menu.no_matches=Geen wedstryde vir {arg1} nie. +option.no=Nee +option.yes=Ja +inventory.cant_carry_any_more_item=Jy kan nie meer {item} dra nie. +inventory.menu.prompt=Voorraadkieslys. {option} +inventory.menu.no_options=Voorraadkieslys. Geen opsies nie. +inventory.option.personal=Persoonlike voorraad +inventory.option.base_storage=Basisberging +inventory.count_set_to_slot={name} telling gestel op gleuf {slot}. +inventory.need_quiver_for_arrows=Jy het 'n koker nodig om pyle te dra. +search.found_item={item} gevind. +search.found_nothing=Niks gevind nie. +pickup.item={item} opgetel. +storage.window_title=Voorraad +storage.transfer_prompt={prompt} (maks {max}) +storage.deposit_how_many=Hoeveel wil jy deponeer? +storage.withdraw_how_many=Hoeveel wil jy onttrek? +storage.nothing_to_deposit=Niks om te deponeer nie. +storage.nothing_to_withdraw=Niks om te onttrek nie. +storage.runed_cannot_deposit=Geruinde items kan nie in berging gedeponeer word nie. +storage.item_full=Berging vir daardie item is vol. +storage.no_storage_built=Geen berging gebou nie. +storage.menu_title=Basisberging. +storage.menu.prompt=Basisberging. {option} +storage.menu.no_options=Basisberging. Geen opsies nie. +crafting.require.fire_within_three_clay_pot=Jy het 'n vuur binne 3 teëls nodig om 'n kleipot te maak. +crafting.require.fire_within_three_clay_pots=Jy het 'n vuur binne 3 teëls nodig om kleipotte te maak. +crafting.require.fire_within_three_bowstring=Jy het 'n vuur binne 3 teëls nodig om 'n boogstring te maak. +crafting.require.altar_incense=Jy het 'n altaar nodig om wierook te maak. +crafting.require.fire_within_three_smoke_fish=Jy het 'n vuur binne 3 teëls nodig om vis te rook. +crafting.require.fire_within_three_butcher=Jy het 'n vuur binne 3 teëls nodig om te slag. +character.slot.head=kop +character.slot.torso=bolyf +character.slot.arms=arms +character.slot.hands=hande +character.slot.legs=bene +character.slot.feet=voete +character.menu.prompt=Karakterinligting. {option} +character.menu.no_options=Karakterinligting. Geen opsies nie. +character.pet.no_pet=Geen troeteldier nie. +character.pet.abandon_confirm=Wil jy regtig jou troeteldier verlaat? {option} +character.pet.unconscious_cannot_abandon=Jou {pet} is bewusteloos. Jy kan dit nie nou verlaat nie. +item.label.items=voorwerpe +item.label.item=voorwerp +item.label.unknown=Onbekend +equipment.menu.equipped_suffix=(toegerus) +equipment.name.none=Geen +equipment.name.unknown=Onbekend +equipment.name.spear=Speer +equipment.name.stone_axe=Klipbyl +equipment.name.sling=Slinger +equipment.name.bow=Boog +equipment.name.skin_hat=Velhoed +equipment.name.skin_gloves=Velhandskoene +equipment.name.skin_pants=Velbroek +equipment.name.skin_tunic=Veltoonika +equipment.name.moccasins=Mokkasiene +equipment.name.skin_pouch=Velsakkie +equipment.name.backpack=Rugsak +equipment.name.fishing_pole=Visstok +equipment.name_plural.spears=Spere +equipment.name_plural.stone_axes=Klipbyle +equipment.name_plural.slings=Slingers +equipment.name_plural.bows=Boë +equipment.name_plural.skin_hats=Velhoede +equipment.name_plural.skin_gloves=Velhandskoene +equipment.name_plural.skin_pants=Velbroeke +equipment.name_plural.skin_tunics=Veltoonikas +equipment.name_plural.moccasins=Mokkasiene +equipment.name_plural.skin_pouches=Velsakkies +equipment.name_plural.backpacks=Rugsakke +equipment.name_plural.fishing_poles=Visstokke +equipment.name_plural.items=Voorwerpe +environment.tree.fell=Boom het geval! +environment.tree.fell_with_loot=Boom het geval! Kry {sticks} {sticks_label}, {vines} {vines_label}, en {logs} {logs_label}. +environment.tree.inventory_full=Voorraad vol. +crafting.menu.prompt=Handwerkkieslys. {option} +crafting.category.weapons=Wapens +crafting.category.tools=Gereedskap +crafting.category.materials=Materiaal +crafting.category.clothing=Klere +crafting.category.buildings=Geboue +crafting.category.barricade=Barrikade +crafting.category.runes=Runes +crafting.weapons.prompt=Wapens. {option} +crafting.weapons.option.spear=Speer (1 Stok, 1 Rankplant, 1 Klip) [Vereis Mes] +crafting.weapons.option.sling=Slinger (1 Vel, 2 Rankplante) +crafting.weapons.option.bow=Boog (1 Stok, 1 Boogstring) +crafting.tools.prompt=Gereedskap. {option} +crafting.tools.option.stone_knife=Klipmes (2 Klippe) +crafting.tools.option.snare=Strik (1 Stok, 2 Rankplante) +crafting.tools.option.stone_axe=Klipbyl (1 Stok, 1 Rankplant, 2 Klippe) [Vereis Mes] +crafting.tools.option.fishing_pole=Visstok (1 Stok, 2 Rankplante) +crafting.tools.option.rope=Tou (3 Rankplante) +crafting.tools.option.quiver=Koker (2 Vel, 2 Rankplante) +crafting.tools.option.canoe=Kano (4 Stamme, 11 Stokke, 11 Rankplante, 6 Vel, 2 Toue, 6 Riette) +crafting.tools.option.reed_basket=Rietmandjie (3 Riette) +crafting.tools.option.clay_pot=Kleipot (3 Klei) +crafting.materials.prompt=Materiaal. {option} +crafting.materials.option.butcher_game=Slag Wild [Vereis Wild, Mes, Vuur naby] +crafting.materials.option.smoke_fish=Rook Vis (1 Vis, 1 Stok) [Vereis Vuur naby] +crafting.materials.option.arrows=Pyle (2 Stokke, 4 Vere, 2 Klippe) [Vereis Koker] +crafting.materials.option.bowstring=Boogstring (3 Sening) [Vereis Vuur naby] +crafting.materials.option.incense=Wierook (6 Stokke, 2 Rankplante, 1 Riet) [Vereis Altaar] +crafting.clothing.prompt=Klere. {option} +crafting.clothing.option.skin_hat=Velhoed (1 Vel, 1 Rankplant) +crafting.clothing.option.skin_gloves=Velhandskoene (1 Vel, 1 Rankplant) +crafting.clothing.option.skin_pants=Velbroek (6 Vel, 3 Rankplante) +crafting.clothing.option.skin_tunic=Veltoonika (4 Vel, 2 Rankplante) +crafting.clothing.option.moccasins=Mokkasiene (2 Vel, 1 Rankplant) +crafting.clothing.option.skin_pouch=Velsakkie (2 Vel, 1 Rankplant) +crafting.clothing.option.backpack=Rugsak (11 Vel, 5 Rankplante, 4 Velsakkies) +crafting.buildings.prompt=Geboue. {option} +crafting.buildings.option.firepit=Vuurherd (9 Klippe) +crafting.buildings.option.fire=Vuur (2 Stokke, 1 Stam) [Vereis Vuurherd] +crafting.buildings.option.herb_garden=Kruietuin (9 Klippe, 3 Rankplante, 2 Stamme) [Slegs Basis] +crafting.buildings.option.storage_upgrade_1=Gradeer Berging op (6 Stamme, 9 Klippe, 8 Rankplante) [Slegs Basis, 50 elk] +crafting.buildings.option.storage_upgrade_2=Gradeer Berging op (12 Stamme, 18 Klippe, 16 Rankplante) [Slegs Basis, 100 elk] +crafting.buildings.option.pasture=Weiveld (8 Stamme, 18 Toue) [Slegs Basis, Vereis Bergingopgradering] +crafting.buildings.option.stable=Stal (10 Stamme, 15 Klippe, 10 Rankplante) [Slegs Basis, Vereis Bergingopgradering] +crafting.buildings.option.altar=Altaar (9 Klippe, 3 Stokke) [Slegs Basis] +crafting.barricade.prompt=Barrikade. {option} +crafting.barricade.option.reinforce_sticks=Versterk met stokke ({cost} stokke, +{health} gesondheid) +crafting.barricade.option.reinforce_vines=Versterk met rankplante ({cost} rankplante, +{health} gesondheid) +crafting.barricade.option.reinforce_log=Versterk met stam ({cost} stam, +{health} gesondheid) +crafting.barricade.option.reinforce_stones=Versterk met klippe ({cost} klippe, +{health} gesondheid) +crafting.missing=Ontbreek: {requirements} +crafting.requirement.stone_knife=Klipmes +crafting.requirement.game=Wild +crafting.requirement.favor=guns +crafting.requirement.resources=hulpbronne +msg.00884428322b=put +msg.01a159425acf=breek strik +msg.01af99b118c8={arg1} Fylgja +msg.03268c7d7239=rugsakke +msg.03976920927e=tou +msg.03ff0c460a1f=Geen opsies nie. +msg.0406529811c7=Klein Wild +msg.041d8413712d='n Ongeluklogboek is na jou stoordatalêer geskryf. +msg.04a8916a9494=vampier1 +msg.0532db2192fa=wierook +msg.05c95b66bd57='n Inwoner se mes het gebreek tydens die slag. +msg.066723a92e94==== Oorwinningsbelonings === +msg.076a11174ca0=Geen spasie vir uitsette nie. +msg.07e9ab246cdd=Nadat jy hierdie skerm toegemaak het, kan jy die notas oefen. +msg.08b42842d5bc=sening +msg.093440631290=Jy het reeds die Rune van Snelheid bemeester. +msg.095b4ad6f082=Geruinde items kan nie geoffer word nie. +msg.096539f23463=Die oorlogstromme slaan! Berei voor vir geveg! +msg.09c28ae8f83b=Klippe +msg.09d016686864=Stal nie gebou nie +msg.0a133b0ecad2='n Visstok gemaak. +msg.0a3bdcef1906=Rietmandjies +msg.0a9d6438c818==== Betowerde Melodie === +msg.0addf2ff4572={arg1} wierook gebrand. +{arg2} uur. +msg.0b3d8d5cdca6=- Bereik die basis en vernietig die barrikade +msg.0b78c6a3e407=Weiveld moet in die basis gebou word. +msg.0ba82f2cdb6c=Jou troeteldier is op pad. +msg.0bcd9af79f2d=woud +msg.0c7789cf9a52=handwerk voltooi +msg.0c976639507d=Voer vuur met stok +msg.0cb01a9fb609='n Klipbyl gemaak. +msg.0cf604cb001b=Pyle +msg.0d1ec2e04286=slinger swaai +msg.0da18cb6fbe2='n Rugsak gemaak. +msg.0e16a11cda9a=messe +msg.0e2e63241760=Berging moet in die basis gebou word. +msg.0e57fb642209=speer +msg.0eb796e65c39=Slingers +msg.0f5211918a6f=Eenhoringjag (Bergbaas) +msg.0f9af12cbb82=velsakkie +msg.10065b4cd62c=Plaas Strik +msg.10adaed44a3e=Daar is reeds 'n strik hier. +msg.114e55e657be=Jy gooi 'n armvol stokke in die vuur. +msg.118e74882896=Kruietuin kan slegs in die basisarea gebou word. +msg.125769e87318=Nie genoeg rankplante nie. +msg.126dd3b70a5c=Stamme +msg.131bd02673e5=Aksiekieslys. +msg.136e4cb03bd5=Kon nie lêer stoor nie +msg.13cd01ceccd3={arg1} rankplante in die vuur gegooi. +msg.14056a0402ef=Jou {arg1} het herstel van sy beserings. +msg.14272bbf4f20=Geen vuur naby nie. +msg.145ba8a7a0f2=kokers +msg.14f5f5ff4f76=Geen oorlewendes gevind nie. +msg.15e5059dafbc=Nie genoeg kokerkapasiteit vir pyle nie. +msg.16134a4250f7=wild val +msg.162bb07757cf=Stal of berging nie gebou nie. Geen perde is gevang nie. +msg.16a233f44489=- Luister vir oop grafte wat nader kom (word harder) +msg.16cd07c0a9b6=Barrikade {arg1} keer versterk met stokke. Gesondheid nou {arg2}. +msg.16d29946de8e={arg1} toegerus. +msg.17213930ddb9=Vuur by x {arg1} y {arg2} word laag! +msg.176b9a0c8398=Wil jy regtig verlaat? +msg.17808e3775e1=wierookstokkie +msg.1792abbb7eb8=Kies toerusting om met {arg1} te graveer. {arg2} +msg.17b5b2a286cc==== Skelet-bard === +msg.17c677526bd6=Druk Spasie om te gooi. +msg.18b837bb6f43=vlermuis1 +msg.18c77e824a2a=jeti sterf +msg.1a25b0443252=Beer geslag. Het vleis, 3 velle, en 2 senings gekry. +msg.1a853cbfbc36=bandiet sterf +msg.1af5fdafa634=Besig met handwerk... +msg.1bb8ef2886a3=Basiese ondooie. +msg.1be2a44cb53d=nag +msg.1bfa6bc809d4==== Bandiete-skuilplek === +msg.1c2272bc0140=Jy het ingeval. Telling {arg1}. +msg.1c5cbc1424ec=Nie genoeg klippe nie. +msg.1c7022965d1a=velhandskoen +msg.1d15d0d5c98d=Berging gebou. Totaal items {arg1} +msg.1d493d7011a0=Geen rankplante om vuur te voer nie. +msg.1d61343b9660=Berging nie gebou nie. Geen itembelonings nie. +msg.1da840244989=vuur +msg.1db76ac85135=slinger +msg.20a5cf945886=Jou {arg1} is bewusteloos. Jy kan dit nie nou verlaat nie. +msg.21192381c66d='n {arg1} het uit jou strik ontsnap by x {arg2} y 0! +msg.21b534113694=Soekbelonings +msg.226f7d1cdeb1=Gaan visstok na +msg.237b5017397b=byl +msg.244b7f948d5d=Die {arg1} inval het geëindig. +msg.2455422b697c=Jy het reeds vandag 'n avontuur probeer. +msg.245c89590526=- Wanneer die brug ineenstort met die Eenhoring daarop, wen jy! +msg.247e80c6393a=Jy het gesterf. +msg.24d4bced9a39=Klim af. +msg.25038d9da464=oos +msg.25296a531b53=Oorwinning! +msg.25db7a4ec692=Vee is herwin en by jou weiveld gevoeg. +msg.2630bd23d7d4=veilig om te spring +msg.2648db667f58=speer tref +msg.26c2f4289162=Laai speletjie. Kies karakter. Druk Delete om 'n stoorlêer te verwyder. +msg.270dcc07d3ae=donderweer medium +msg.271e9a568c0e=spring +msg.2727056eb878=Weiveld gebou. Vee {arg1} van {arg2} +msg.273d131f0c8e=Geen kennisgewings nie. +msg.27a582f91d6b={arg1} val aan vanaf {arg2}! +msg.27bbfb11a294=Herhaal die magiese patroon om guns van die gode te verdien. +msg.27e4cdae51e3=vampier4 +msg.27f1dd42841b=- Druk Enter om te bevestig +msg.27f5254de319=Berging nie gebou nie +msg.27f76f96867d={arg1}: +{arg2}. +msg.282b619d84bc=- Or veg direk teen die Eenhoring (dit het massiewe gesondheid) +msg.285df50e183e=- Druk SPASIE om jou speer te gooi wanneer die vlermuisgeluide in die middel is +msg.29a2d9e8aadb=Strik hou 'n {arg1} vas. +msg.29a828259ef4={arg1} Velhoede gemaak. +msg.29d1fab76b47='n Rietmandjie gemaak. +msg.29f291640516=Soekinligting +msg.2a72e1170f55=Geen perde is gevang nie. +msg.2afb87ae1752=- Jy hardloop outomaties, spoed neem toe met tyd +msg.2b436c9ddbbb=Eend geslag. Het 1 vleis, vere, en dons gekry. +msg.2b8ff62c083e=Moet in basis wees om altaar te gebruik. +msg.2badd283a63b=Jy het reeds die {arg1} Fylgja ontsluit. +msg.2bb11d7c39dc='n Vyand is binne bereik van jou huidige wapen. +msg.2bf7b1d59f34=eenhoring val +msg.2c6c80931cd1=Velhoede +msg.2cd0d08a829a={arg1} wierook gemaak. +msg.2db4abf99659=Nuwe speletjie begin. +msg.2e094dd0a2ab=speer swaai +msg.2efa88864653=Die bumerang het jou getref.{arg1} +msg.3030e8ec7633=Vis +msg.30680e90d434={arg1} vis gerook in {arg2} gerookte vis. +msg.311f421748f9=Jy het 'n koker nodig om pyle te maak. +msg.31342e72b48c={arg1} vertrek. +msg.32b43edbf3b0=Berging moet opgegradeer word voor 'n weiveld. +msg.32c8e74e0055='n Koker gemaak. +msg.33bc40d8201e=- Na die deuntjie, kies die aantal notas +msg.33c75a645734='n Boogstring gemaak. +msg.33c89b4bdc4e=koker +msg.34daac1528e0=kieslysbeweging +msg.34e4ec0621b4=Berging moet opgegradeer word voor 'n stal. +msg.34fcedc354d9=visstok +msg.363b1d2abd57=Kos in berging {arg1} vleis, {arg2} gerookte vis, {arg3} mandjies vrugte en neute. Totaal {arg4}. Daaglikse verbruik {arg5} +msg.364118a099cd=Eenhoring-oorwinning +msg.36e876dbcc17=Jy het 'n {arg1}-verbintenis met die {arg2}. +msg.37423f3bd00f=velle +msg.37bb70fed953=Strik is gestel en leeg. +msg.37ef0562902c=rankplant +msg.381aea2617cb=Wierook +msg.38a7045d078c=donderweer laag +msg.38eb0fe3f78a=pyl vlieg +msg.39268e59f7ba=Strik is gestel maar nog nie aktief nie. +msg.3976f81bbfef=Jou {arg1} vertrek. +msg.39784ab5ac68=stukkie sening +msg.39a531d92bbe=pyl +msg.3a7d9767b123=voorwerp +msg.3ba80d6f0cfb=Runes. {arg1} +msg.3bc5191d2dc2=Begin boom klim. Hoogte is {arg1} voet. +msg.3c32451c9c71=velsakkies +msg.3ce35dadcc67=Stal is vol. Geen perde is gevang nie. +msg.3cebb4c50866=visstok +msg.3dd6d1aafd1c={arg1} Velhandskoene gemaak. +msg.3e024b87e650=harde klip +msg.3e03229d39e4=Velle +msg.3e2cee02d8c1=Die lug begin ophelder. +msg.3e575fbcccad=Geen geboue beskikbaar nie. +msg.3e919b422247=Vuur by x {arg1} y {arg2} het doodgegaan. +msg.3eb9cd52ec8d=velhandskoene +msg.3f098cc2c738=Gepouseer. Druk Backspace om voort te gaan. +msg.3f35ee808cb2={arg1} Veltoonikas gemaak. +msg.3f638a51cb47=E of U - Vierde nota (hoogste toonhoogte) +msg.3fab86f11579=Hierdie boom is leeg. +msg.3ffc8c72cadf="'n {arg1} het ontsnap terwyl 'n inwoner die strik by x {arg2} nagegaan het. +msg.4010ceb056dd=donderweer hoog +msg.403654dac805=been4 +msg.4037ea7ad431=spere +msg.408a612a4167=skis1 +msg.409251fa0d30=Altaar moet in die basis gebou word. +msg.412fed81f58a=rietmandjie +msg.417705583821=wind middel +msg.41a682cadfe3=vlieg +msg.423b7f343fb8=Vis aan die lyn. +msg.4248c4c1f5a9=Wierook brand. {arg1} uur oor. +msg.425dbf90058e={arg1} Slingers gemaak. +msg.4272004d80b4=beerkarkas +msg.427944aa680d=Velsakkies +msg.42b2620d1afa=- Akkurate gooi verdien 2 punte elk +msg.42da7d0b84a8=- Elke suksesvolle sprong verdien 2 punte +msg.434d81f4ecc4=Kon nie lêer oopmaak nie: {arg1} +msg.43696d3d5fb8={arg1} gegraveer. +msg.437ecef821db=speer mis +msg.43821cae64c5=boogstrings +msg.44328bf27ea3={arg1} stokke in die vuur gegooi. +msg.444d4678962b=val +msg.4525b0152629=Geen toerusting beskikbaar om te graveer nie. +msg.4537c16badd9=boog skiet +msg.455b11a16eb7=strikke +msg.45a604519c0d=Die bandiete het jou neergevel. +msg.45bcf18418f1=Jy moet binne 2 teëls van 'n stroom wees. +msg.46fcdd91c99e=draugrhaugr stap +msg.470f9d20c2b3={arg1} Tou gemaak. +msg.474c797713f3=stamme +msg.4798100ccc16=Skelet-bard voltooi. Jou telling vir al vyf deuntjies is {arg1}. +msg.483a17dd2a17=Strikke +msg.484adfaab5cd={arg1} Boë gemaak. +msg.494f87e3874d=draugrhaugr1 +msg.495f723e9fa1=byle +msg.4a0e932d1bc5=gruis +msg.4a17d4fabe93=velbroeke +msg.4b9b4385b1a9=bumerang +msg.4bce12c3add1=Jy are nie in die basis nie. +msg.4bd5f5ba3c71={arg1} onttrek. +msg.4c0d2469f78b=klippe +msg.4c8578f10e74=Daar is reeds 'n vuurherd in die basis. +msg.4d723065565f=Jy gooi 'n stam in die vuur. +msg.4db797458510={arg1} Kleipotte gemaak. +msg.4e5ee9d0128e=Geen troeteldier nie. +msg.4ea30f60b92f=Rune-gravering kan slegs in die basisarea gedoen word. +msg.4ecf016b2d3f=Daar is reeds 'n vuurherd hier. +msg.4f08301dcb7b=kennisgewing +msg.4f11c6797b8d=vlak water +msg.4f4dcdd4262d=Stal gebou. +msg.4f773e9d5bf0=Boë +msg.4f7f358cd341=gans +msg.4f99d3d5b638=Kleipotte +msg.4ff23d0c3700=Jy het die bumerang gemis.{arg1} +msg.5009c8e190ee=kalkoen +msg.508e8bc0cc57=- Gebruik Op/Af om die getal te verander +msg.5123c22ce6a5=slinger lae reikafstand +msg.514b1402c9f3=Veilig die grond bereik. +msg.515f45b3270f=Die top bereik op {arg1} voet. +msg.52140591224d=Die gode is tevrede met jou oorwinning! {arg1} guns toegeken. +msg.528fb1c4e5fb=Nuwe speletjie +msg.529f0f749181=Geen {arg1} beskikbaar nie. +msg.52f1fd1916c9=Brand wierook +msg.5397e0583f14=Ja +msg.53da80cba4ff={arg1} en strik versamel. +msg.5460f2e3c43e=papdruk +msg.546330ee64db=visstok breek +msg.546f2ff04577=Strik gestel. +msg.54fc2d657ed6=Draugnorak +msg.55748cf43121=vampier3 +msg.55c9d2b3e8a4='n Veltoonika gemaak. +msg.5665ef30ef4d=Vlermuise val aan! Gooi jou speer om te verdedig. +msg.57b0465e575a="'n Bergreeks is in die ooste ontdek! +msg.57e2db156a64=Geen strik naby nie. +msg.57e457c02ba4=Voer vuur met stam +msg.584227d57290=Riette +msg.5906872f9bb6=vampier2 +msg.596350ae8387="'n Riet gevind. +msg.599096c44c5b=Beerkarkasse +msg.5993f9da16fc=Leer geluide. +msg.59b74cafdedb=Vlermuisinval voltooi. Telling {arg1}. +msg.59eab77f00cc="'n {arg1} word honger. +msg.5a7c581c0427={arg1} Strikke gemaak. +msg.5b61e4982b59=skis sterf +msg.5bdb7fa2223d=Vere +msg.5be2899acbe8=Die inwoners se doel vervaag. +msg.5c2434ecd90f=riette +msg.5cac31311668=Weiveld is vol. Geen vee is herwin nie. +msg.5cc7429297c8=wat met 'n byl vernietig kan word. +msg.5cda1b25dd32="'n {arg1} is honger en reageer nie. +msg.5d1fa9e5a5ef="'n Inwoner is geneem. +msg.5de6d49b4e89=Die area het uitgebrei! Nuwe gebied in die ooste ontdek. +msg.5dfc869b7588=gooi krag +msg.5e09b31082c5=Geen strikke om te plaas nie. +msg.5ea84b1201bb={arg1} guns gee jou die oë van 'n arend. +msg.5f3f3a85393b=kieslys kies +msg.600007c39e3f=Geen avonture beskikbaar in die basis nie. +msg.612e052d92d2=Barrikade is reeds by volle gesondheid. +msg.61b1ce299103={arg1}. {arg2} oor. +msg.623f78278373=Barrikade gesondheid {arg1} van {arg2} +msg.62583aa87a59=Weiveld of berging nie gebou nie. Geen vee is herwin nie. +msg.627a5b9e6322=klim boom +msg.62b1e8703ede='n Speer gemaak. +msg.62fac2f438bb=valk +msg.64875fcccaac=vis +msg.64b945d9072e=Kruietuin nie gebou nie +msg.656dac78b857=Jy het {arg1} bumerangs gevang vir 'n totaal van {arg2} punte. +msg.65eb0fb6ab40=velbroek +msg.666d7125f17b=Geen soektogte beskikbaar nie. +msg.66fd9a7e91f8=rankplante +msg.67ae077e6458=Kruietuin gebou +msg.686021c25738=been3 +msg.6861fd95f4f7=- Daar is 5 beurte +msg.6a26c7cfd66b={arg1} gedeponeer. +msg.6a33218f6325=Jy het die bumerang {arg1}. {arg2} punte.{arg3} +msg.6c265b98505c=byl swaai +msg.6cbfe49b4adf=Kon nie geluid laai nie. +msg.6cf4935744c0=Kano's +msg.6d18e60e7952=basis vernietig +msg.6d4a0243dbfd=Lêer suksesvol gestoor +msg.6d9c2a31fbbc=kleipotte +msg.6e247a17f21a=Druk Enter wanneer gereed om te begin, of Escape om te kanselleer. +msg.6e32618ee300=mes +msg.6fdc01a81c89=Jy kan nou toerusting met hierdie rune graveer by die handwerkkieslys. +msg.704e634dd69f=tref grond +msg.708019666e3c={arg1} Kokers gemaak. +msg.70a5f57b592d=Geen altaar gebou nie. +msg.714165bec103=stel strik +msg.717d7fcc68b7=- Daar is 5 rondtes, tot 4 punte elk +msg.71aad5f523eb=velhoede +msg.731152a028c6=Gesluit. +msg.757117781964=Inwoners het {arg1} mandjies vrugte en neute versamel. +msg.7680f62917b1=- Druk weer Spasie wanneer dit naby klink +msg.76c1490aad8c=Geen wierook om te brand nie. +msg.77346d0447da=dons +msg.7748b68d141a=diep woud +msg.778a4ff05e50=Kalkoen geslag. Het 1 vleis en vere gekry. +msg.7822e0af08c8=Altaar reeds gebou. +msg.78f036ea9ebd=Die dier storm onvermoeid heen en weer. +msg.7921110eb7e1=besig met handwerk +msg.7968afd0c044=visstokke +msg.7a61d6b17282=Jy het 'n kleipot nodig om wierook te brand. +msg.7b50bfc0ef4b={arg1} Rietmandjies gemaak. +msg.7babc233de26=stam +msg.7c49f713a0fb=pyle +msg.7c57f16628c7="'n Klip gevind. +msg.7cc940c217a7=Klim teen {arg1}. {arg2} voet. +msg.7d2cf35450e3=Luister na die melodie en tel die notas. +msg.7d88f361f3e8=zombie1 +msg.7e83de91b4a2=bandietvrou sterf +msg.7f6990bc78a8={arg1} Klipbyle gemaak. +msg.7f7e3bbf8864="'n Vyand is nie meer binne bereik van jou huidige wapen nie. +msg.7fae743693f4=Rugsakke +msg.802aa6576577=Laai Spel +msg.803e4c41bbd2=stokke +msg.80655da8d80a=boom +msg.806baaf8e039=kano's +msg.816c52fd2bdd=Nee +msg.81df04a81e24={arg1} guns skyn op jou. {arg2} +msg.823be948275f=- Beter tydsberekening verdien meer punte (tot 4) +msg.8257ec29507d={arg1} stamme in die vuur gegooi. +msg.837affd1b6ba=Barrikade {arg1} keer versterk met klippe. Gesondheid nou {arg2}. +msg.83865f248682=Kragtiger ondooie. Hulle verloor jou nie uit die oog wanneer jy in die basis is nie. +msg.83a5c4369598='n Klipmes gemaak. +msg.83fa126f6e3f={arg1} gestel op gleuf {arg2}. +msg.8420d3438dd8=Fylgja-kieslys. +msg.8443de859917=wind hoog +msg.849a4f4a399d=Jy vlug uit die ryk van die dooies! +msg.84c5ff34e186={arg1} Klipmesse gemaak. +msg.8551c25cde92=Die wierook het uitgebrand. +msg.8569a552d825=x {arg1}, terrein {arg2}. Basis {arg3} teëls oos. Barrikade {arg4} van {arg5}. +msg.85a177a79f1d=been2 +msg.85f15bc57d6c=veer +msg.862021235188=kano +msg.862614facb79=Geen kos nie, inwoners is honger. +msg.8626f8c56627=Velhandskoene gemaak. +msg.86290fe7e711=been5 +msg.86e257c3efcb=Oudste kennisgewing. {arg1} +msg.87383ce4344d=Boogstrings +msg.87cdb5c91437=toue +msg.87ee697acb01=Altaar gebou. +msg.8818f1d55166={arg1} glans vul inwoners met doelgerigtheid. +msg.885cda3d707c=Jy vlug uit die skuilplek. +msg.88749dbbbf09==== Vlermuisinval === +msg.88a8bee5206e=- Lok die Eenhoring op die brug +msg.8aab96177c7d=Gaan strik na +msg.8b9881d57a1a=Hierdie area het niks oor nie. +msg.8c3fa20e31d0=Jy het {arg1} notas ooreengestem. Telling {arg2}. +msg.8d0ad3cf8648=Stoorlêer uitgevee. +msg.8d6c04ec96c7=Die vis is nog aan die lyn. +msg.8e19643ce0e6={arg1} uitgetrek. +msg.8ee83610f314=mokkasiene +msg.8f1379e43b0f=Velbroeke +msg.8f69276b9332=Gooi 'n bumerang en vang dit op die terugtog. +msg.8f993ac20023=Jy gaan 'n nou bergpas binne. 'n Massiewe Eenhoring versper jou pad! +msg.8feaa2e73a19=Basisinligting. {arg1} +msg.9007bd6e52db=Nie genoeg stokke nie. +msg.90c991ce4eec=Altaar. Guns {arg1}. +msg.917ee46db0cd=by +msg.922b0aa0ef1a=Barrikade {arg1} keer versterk met rankplante. Gesondheid nou {arg2}. +msg.924e0eb6b6f1=speler manlike skade +msg.9281d2fe5f08={arg1} guns skyn op jou. Jy voel vinnig vir 'n rukkie. +msg.92849349ef1f="'n Perd sluit by jou stal aan. +msg.92abc6d6953f=klim tou +msg.92b702e15649=Jy het die {arg1} Fylgja ontsluit! +msg.93682970720f=Wierook gemaak. +msg.9379874d592b=Altaar nie gebou nie. Geen guns toegeken nie. +msg.93852d36202b={arg1} Mokkasiene gemaak. +msg.93894d088f6a=- Gebruik jou byl om 'n brugstut te vernietig +msg.93fa0af97c4f=Daar is reeds 'n kruietuin in die basis. +msg.93fc8e4d1331={arg1} {arg2} gegraveer met {arg3}. +msg.946466f6c963=stok +msg.94c221dc0888=Oefenmodus. Druk Enter om te begin, Escape om te kanselleer. +msg.957f554c342a=beer +msg.9612a65234f9={arg1} gevang in strik by x {arg2} y 0! +msg.96901dc80a5b=Geen materiaal om die barrikade te versterk nie. +msg.96b0a63888d1=Kan nie strikke in die basisarea plaas nie. +msg.97c59037a63c="'n Oorlewende sluit by jou basis aan. +msg.97dcc91b2ee2=Jy het op jou strik getrap en dit gebreek! +msg.988157738a23=Geen avonture in hierdie area gevind nie. +msg.98ae12aa6a63=Jy stop visvang. +msg.995d4e06ae4e=gerookte vis +msg.998b61086fa7=Jy het 'n item opgetel waarvoor daar geen spesifieke geluid is nie. +msg.9afdb7e61391=- Druk Spasie om te gooi +msg.9b64062b2074=Bandiete-skuilplek +msg.9b81947e1398=Inwoners het 'n aanvallende {arg1} doodgemaak. +msg.9bbcfc6c8f9d=bandiet4 +msg.9c19f78d931d=been6 +msg.9dd40321998e=vlermuis2 +msg.9ea4f66d0740=Gaan vuur na +msg.9fcdb8881bce=Jy vlug uit die ontmoeting. +msg.a040b931b9bb=Hoe om te speel: +msg.a0844149a18c=Jy het {arg1} notas ingevoer en die werklike getal was {arg2}. {arg3} punte. Druk Enter om voort te gaan. +msg.a153dba5e67a=diep water +msg.a2620cbc10f5=dag +msg.a294ccba07b9=Vuurherd hier gebou. +msg.a2cc4be07e5e=Avontuurkieslys. +msg.a2d7f5d60d98=klein wild +msg.a2f3a6c42a8f=allerlei +msg.a3347af174f3=Weiveld nie gebou nie +msg.a3cb738850fa=wolf +msg.a3f2439ef01a=velhoed +msg.a56be51786dc=been1 +msg.a570b024087d=Altyd honger, hulle voed op jou inwoners. +msg.a641f39a6317=- Die hardloop eindig wanneer jy in 'n graf val +msg.a646f0dcb735=Guns toegeken: {arg1}. +msg.a6b2cd34521f=Die son gaan onder. +msg.a6c587a6a19d="'n Vis is aan die lyn! +msg.a6caf0c3771d=slingers +msg.a6e2492f34b6==== Ontsnap uit Hel === +msg.a75c9ab9b807=Stal reeds gebou. +msg.a79dc18e77d9=- Jy het 10 pogings +msg.a7cb7754af6c=Nuutste kennisgewing. {arg1} +msg.a95eab2b45da=- Spring (OP-pyltjie) om te verhoed dat jy vertrap word +msg.a9cf87f2edd7={arg1} Velbroeke gemaak. +msg.aa3a1916166d=goblin1 +msg.aa5edb7494f3=kleipot +msg.ab2244bc6027=genesingsrol +msg.abd02188f443=Inwoner het {arg1} uit strik by x {arg2} y 0 gehaal en dit herstel. +msg.abe73973cc7c=Kon nie stoorlêer laai nie. +msg.ac36af678d78=Kontroles: +msg.ac66e08b54d7=Hervat. +msg.ac8ccb330272="'n {arg1} sluit by jou aan. +msg.ad34f00c46cd=mokkasien +msg.ad36d64ad3a3=goblin sterf +msg.ad610e155539=Die spoedseën vervaag. +msg.ade25255fbc1='n Kano gemaak. +msg.ae534dc25ad3=Inwoners het die barrikade herstel. +{arg1} gesondheid. +msg.af1bd00fc4be=Niks om te offer nie. +msg.af8df8d37140={arg1} Kano's gemaak. +msg.af93482d73f0=genesingsrolle +msg.aff5100bebc5=swart kat +msg.b0b80b668953={arg1} Spere gemaak. +msg.b0cca9f9205a=Stokke +msg.b0f6aa37f763=Rankplante +msg.b11aa393c8a8=Mokkasiene +msg.b23d59c4bbce='n Slinger gemaak. +msg.b31837f2fc5b=begin visvang +msg.b32fc272dc0d=- Luister vir vlermuise wat van links of regs verbyvlieg +msg.b38e4cff23de=Inwoner het 'n vis gevang en by berging gevoeg. +msg.b46513dbc687={arg1} {arg2} onttrek. +msg.b48d0661f0da=Veltoonikas +msg.b4a2314b1d80=Hierdie boom is afgekap. +msg.b4a9d19a4e85=Kies aantal notas. {arg1}. +msg.b5d5cc084a95=bandiet3 +msg.b5f1924def3d=Inwoner het 'n vis gerook in {arg1} gerookte vis. +msg.b66ee1230dac=Barrikade versterk met klippe. +{arg1} gesondheid. Nou {arg2} van {arg3}. +msg.b73a0709297f=roep troeteldier +msg.b774102c89b5=Messe +msg.b8de1d1a935f=Inwoners het 'n aanvallende bandiet doodgemaak. +msg.b94e9f3d7e00=sneeu +msg.ba3af68702a3=Rune van Vernietiging geleer! +msg.ba7d7dfe330b=Mandjies vrugte en neute +msg.bad2fcec0edf=Mokkasiene gemaak. +msg.bb0ea68db3b4=mandjie vrugte en neute +msg.bb40f75a9c60=vleis +msg.bbc29b76c59b=gras +msg.bbfd1384acaf='n Kleipot gemaak. +msg.bd4796595f1f=Geen stokke om vuur te voer nie. +msg.bd573ed65ffe=Geen vis aan die lyn nie. +msg.bdebda25dad9=pyl tref +msg.bf57e001639c='n Velsakkie gemaak. +msg.bf93e5ce8bc1=Dons +msg.bfc7e2fc2280=Barrikade versterk met stam. +{arg1} gesondheid. Nou {arg2} van {arg3}. +msg.c01409b3a728=Wanneer jy gooi, los kontrole wanneer bo water. Wanneer jy vang, los wanneer geluid bo speler is. +msg.c05feaca7269=Geen items is herwin nie. +msg.c1104d82c0bd=strik +msg.c2ff4bc695b7=Item nie beskikbaar nie. +msg.c3080306943b=Toerustingkieslys. +msg.c3567877be0e={arg1} pyle gemaak. +msg.c418fde85a0d='n Velhoed gemaak. +msg.c477acc3f80b=Jy kan slegs {arg1} pyle dra met jou huidige kokers. +msg.c48d642737b2=Woon in bergagtige streke. +msg.c4f7e9c21601=Gerookte Vis +msg.c50641e24962=Jou gooi het die water gemis en in die nabygeleë plantegroei beland waar dit onwaarskynlik is om vis te lok. +msg.c50d661da7cf=Die barrikade het geval! +msg.c52888225c69=val +msg.c5565f654184=Kruietuin gebou. Die basis genees nou vinniger. +msg.c6186586864e=x {arg1}, terrein {arg2}. Eenhoring kyk na {arg3} +msg.c65e5dfc5ba7=bandiet1 +msg.c72b7844929d=Bandiete verslaan: {arg1}. +msg.c82e3d7279ef=stroom +msg.c86ace9b97f8=Stal gebou. Perde {arg1} van {arg2} +msg.c8d882f9098c=Inwoners het {arg1} visse gevang en by berging gevoeg. +msg.c8da1c4f1714=Word laer in toonhoogte soos die val vorder. +msg.c8fee26f6ee3=Geweier. +msg.c90399f41b3f=Vleis +msg.c9532ab465fb=been7 +msg.cb6c212139a1=Rune van Snelheid geleer! +msg.cb8b9677952a=Bergingbelonings: +msg.cb8e70ab2bcc=beerkarkasse +msg.cc167fb645e4=boogstring +msg.cc3caa9812e2=Geen stamme om vuur te voer nie. +msg.cc61d0f3de65=rietmandjies +msg.ccb9a6240155=Jy het 'n vuurherd binne 2 teëls nodig om 'n vuur te bou. +msg.cccc5c842adb=byl tref +msg.ccce38999a27=Weiveld reeds gebou. +msg.cd13e6ce9297=Die arend se sig vervaag. +msg.cd3ef7da3f4f={arg1} {arg2} gedeponeer. +msg.cd4c1cc68057=Brand! {arg1} gesondheid oor. +msg.ce13e4341cdc=binne bereik kom +msg.ce3897c6b3be={arg1}: {arg2} +msg.ce5aeca95fb9=item breek +msg.cea88b214070={arg1} {arg2} +msg.cfe2650b4c6e="'n Klip gevind. +msg.d04a59a6a71b=Kontroles: LINKS/REGS om te beweeg, OP om te spring, CTRL om aan te val, ESC om te vlug +msg.d0cb2acd0739=vel +msg.d103663bcfd9=1 {arg1} geoffer. Guns +{arg2}. Totaal {arg3}. +msg.d1c5baa8a36b=Barrikade versterk met stokke. +{arg1} gesondheid. Nou {arg2} van {arg3}. +msg.d34b6217ea75=vyand val +msg.d373510877fd=Velhandskoene +msg.d39008c4a392=Onbehandelde uitsondering +msg.d49f2e760778=rugsak +msg.d537941485fb=R of I - Derde nota +msg.d5dd6ddda4ef=Vuur gebou by vuurherd. +msg.d5e86e1918e8=Niks om toe te rus nie. +msg.d5e89cddb940=Stal moet in die basis gebou word. +msg.d609f615ddae=Geen stoorlêers gevind nie. +msg.d63eba28afc0=wes +msg.d6dc139f529a=inval +msg.d6fd3467a950=jeti1 +msg.d75f36f88797=Byle +msg.d7879bd506af=Geen vee is herwin nie. +msg.d8ea835664e1=Jou basis is by maksimum kapasiteit. +msg.da0b5a461d98=F of K - Eerste nota (laagste toonhoogte) +msg.da11dc5e090c=slinger tref +msg.da93fb9992a3=vampier sterf +msg.dadcebf86f43="'n {arg1} {arg2} gevang. +msg.dadd68431e60=Woon in sneeuagtige streke. +msg.db2a8514a0fc=buite bereik gaan +msg.db476a28fe5b=Soekekieslys. +msg.db56e2e1db9f=Avontuur +msg.db5bbabf1bc3=bandiet2 +msg.dbea5ffbac7b=Draugnorak. Hoofkieslys. +msg.dc5c17f80fbe=Kan dit nie doen nie. +msg.ddea499514d8=Jy het reeds vandag jou Fylgja gebruik. +msg.ddf360815408=Sening +msg.ddfc9c735c48=Bandiete sal dit natuurlik nie sommer so aanvaar nie. +msg.de0dce8ef11c=mandjies vrugte en neute +msg.de2c51fece28=Altaar nie gebou nie +msg.de4e8495f978=voetstap +msg.de6b29cb1808=klei +msg.de752f6fdd8b=vere +msg.df3f34f3f144="'n Skelet genaamd Billy Bones oefen om 'n bard te word. +msg.df63c8846d76=Doelwit: +msg.df93ea08d751={arg1} Velsakkies gemaak. +msg.dfa22c1cada0={arg1} Rugsakke gemaak. +msg.dffaa4b11d31={arg1} {arg2} geoffer. Guns +{arg3}. Totaal {arg4}. +msg.e00d5664a78e=Klei +msg.e0b678c82a60="'n Houtbrug span oor 'n diep kloof vorentoe. Die brug het twee stutte +msg.e0c7f029cfa2=Geen runes nog ontsluit nie. +msg.e142524d7689=Strategie: +msg.e1c16d0490ee=Geen geluide beskikbaar nie. +msg.e1dc1099a799={arg1} Visstokke gemaak. +msg.e253d297aabc=veltoonika +msg.e2e91797da96=Visstokke +msg.e30bfd0c38dc=klip +msg.e31811cdce94=Tou gemaak. +msg.e34161b98188=slegte gooi +msg.e379741bfbd3=Genesingsrolle: +{arg1}. +msg.e3ce3e543239=Kokers +msg.e3d828480491=slinger hoë reikafstand +msg.e4427f09bc09=Sluit hierdie skerm om te begin. +msg.e5a51ddbb159=Voer vuur met rankplant +msg.e5d79aba2048=ondooie inwoner1 +msg.e5f656d506a7=zombie tref +msg.e6526216c064=Jy het die skat geplunder, and die Draugr is ontevrede! +msg.e65fa8960222=Jy het reeds die Rune van Vernietiging bemeester. +msg.e6ae58426f13=been8 +msg.e7409fcb54bc=voer vuur +msg.e74f453392b6=Strik versamel. +msg.e75d3068a5e9=Barrikade versterk met rankplante. +{arg1} gesondheid. Nou {arg2} van {arg3}. +msg.e8002c169040=hout +msg.e8216720d30b=- Druk SPASIE om oor grafte te spring +msg.e8db3a42f86f=Leer geluide +msg.e8e8f4ee7a5a=Jy kan nou wapens met hierdie rune graveer by die handwerkkieslys. +msg.e8fe153261a9=Nie genoeg stamme nie. +msg.e9dd1d268b65=Die basis lê ver na die ooste, bewaak deur 'n barrikade. +msg.ea6bfdb7bd29=Die Eenhoring het jou vertrap. +msg.ea771032e597=Altaar gebou +msg.ead6f13efc8d=Jy moet 'n visstok toegerus hê. +msg.eb9cac43e997="'n Strik gemaak. +msg.ebf229b227f8={arg1} {arg2} van {arg3} +msg.ec465c0c4000=Jy het oop grond voor jou nodig om te storm. +msg.eced46d198ed=Inwoner het 'n mandjie vrugte en neute versamel. +msg.ed989350becb=Hoogte {arg1} bereik. +msg.edbcc0a199d1={arg1} gesondheid. +msg.ee02d6d4c199=Jy het op jou strik getrap! Die {arg1} het ontsnap. +msg.ee200b3d8ae2=Spere +msg.eff5a5f34d9a=eenhoring op brug +msg.f015f25c409b=boog span +msg.f08ee9fe3340=Begin nou. Herhaal die patroon. +msg.f12b2d219fe8=Toue +msg.f137213e72e2="'n Swerm zombies is opgemerk. +msg.f16169ef79aa=Nie heeltemal 'n zombie of 'n vampier nie. Hierdie ondooie onthou dat hulle voorheen 'n huis gehad het, and " +msg.f1c671ae3045=Basisinligting. Geen opsies nie. +msg.f229791ba917={arg1} voet geval! {arg2} skade geneem. {arg3} gesondheid oor. +msg.f305da2c3550==== Vang die Bumerang === +msg.f32ea7a06f91={arg1}, x {arg2}, y {arg3}, terrein {arg4} +msg.f3c5ed579a54=wind laag +msg.f3d964225e83=speler vroulike skade +msg.f3e51f53f970=Weiveld gebou. +msg.f40ad52f4d97=Barrikade {arg1} keer versterk met stam. Gesondheid nou {arg2}. +msg.f48b940f83c7={arg1} geslag. Het 1 vleis en 1 vel gekry. +msg.f50492d2703a=Gans geslag. Het 1 vleis, vere, en dons gekry. +msg.f5cabf873590=riet +msg.f5f6c13f2ceb=Inwoner het {arg1} by berging gevoeg. +msg.f77fc018cb7b=Jy vind 'n versteekte bandiete-basis diep in die woud. +msg.f83b6fe3aebf=Verlaat +msg.f85b4b604c9b={arg1}. +msg.f8909d26fe86=ondooie inwoner sterf +msg.f8b2a45e7e4b='n Boog gemaak. +msg.f8fc573e2d2f=Laai Spel (geen stoorlêers gevind nie) +msg.fa5bd38090c7=boog +msg.fa87b71ab25a=Speletjie gelaai. +msg.fa88a4c8033c=Daar is reeds 'n vuur in die basis. +msg.fb091149bb52=Velbroek gemaak. +msg.fb09c6d50b11="'n Nuwe soeke is beskikbaar: {arg1}. +msg.fb389469701e=Berging opgegradeer. Kapasiteit is nou {arg1} per item. +msg.fb4a022a82b3=eenhoring galop +msg.fbec17cb2fcb=reën +msg.fc1740dbb4ae=Geen wedstryde vir {arg1} nie. +msg.fc1ad6e10ac4={arg1} ({arg2} beskikbaar) +msg.fc5294f9b150={arg1} Boogstrings gemaak. +msg.fca1af3d07b0=Genesingsrolle +msg.fd1a25749457=Klankvolume {arg1}. +msg.fdd0b57cb7b9=Jy gooi 'n paar rankplante en blare in die vuur. +msg.fe0ede7d0360=Berging is volledig opgegradeer. +msg.fe2a9d6606e0=draugnorak +msg.fe406c6e0055=Begin nou. +msg.feea4a3b0a84==== Eenhoringjag === +msg.fefc084f67de=D of J - Tweede nota +msg.ff5ac0b6df7b=boë +msg.ffed2db61296=Vis gerook in {arg1} gerookte vis. diff --git a/lang/en.ini b/lang/en.ini new file mode 100644 index 0000000..5503da8 --- /dev/null +++ b/lang/en.ini @@ -0,0 +1,1531 @@ +; Draugnorak localization catalog +; Copy this file to lang/.ini and translate only the right-hand values. +; Keep keys unchanged. Use placeholders like {arg1}, {count}, {language} exactly as written. + +[meta] +code=en +name=English +native_name=English + +[system] +language.select_prompt=Select your language. +language.selected=Language set to {language}. +language.english_label=English (en) +ui.window_title=Draugnorak +sex.male=Male +sex.female=Female +new_character.choose_sex=Choose your sex. +new_character.enter_name=Enter your name or press Enter for random. +new_character.save_exists_overwrite=Save found for {name}. Overwrite? +load_game.option_with_metadata={name}, {sex}, day {day} +load_game.delete_confirm_base=Are you sure you want to delete the character {name}? +load_game.delete_confirm_with_metadata=Are you sure you want to delete the character {name} gender {sex} days {day}? +load_game.delete_save_heading=Delete Save +load_game.delete_save_failed=Unable to delete save. +quick_slot.no_item_bound=No item bound to slot {slot}. +menu.closed=Closed. +menu.canceled=Canceled. +menu.no_options=No options. +menu.no_matches=No matches for {arg1}. +option.no=No +option.yes=Yes +inventory.cant_carry_any_more_item=You can't carry any more {item}. +inventory.menu.prompt=Inventory menu. {option} +inventory.menu.no_options=Inventory menu. No options. +inventory.option.personal=Personal inventory +inventory.option.base_storage=Base storage +inventory.count_set_to_slot={name} count set to slot {slot}. +inventory.need_quiver_for_arrows=You need a quiver to carry arrows. +search.found_item=Found {item}. +search.found_nothing=Found nothing. +pickup.item=Picked up {item}. +storage.window_title=Inventory +storage.transfer_prompt={prompt} (max {max}) +storage.deposit_how_many=Deposit how many? +storage.withdraw_how_many=Withdraw how many? +storage.nothing_to_deposit=Nothing to deposit. +storage.nothing_to_withdraw=Nothing to withdraw. +storage.runed_cannot_deposit=Runed items cannot be deposited into storage. +storage.item_full=Storage for that item is full. +storage.no_storage_built=No storage built. +storage.menu_title=Base storage. +storage.menu.prompt=Base storage. {option} +storage.menu.no_options=Base storage. No options. +crafting.require.fire_within_three_clay_pot=You need a fire within 3 tiles to craft a clay pot. +crafting.require.fire_within_three_clay_pots=You need a fire within 3 tiles to craft clay pots. +crafting.require.fire_within_three_bowstring=You need a fire within 3 tiles to make bowstring. +crafting.require.altar_incense=You need an altar to craft incense. +crafting.require.fire_within_three_smoke_fish=You need a fire within 3 tiles to smoke fish. +crafting.require.fire_within_three_butcher=You need a fire within 3 tiles to butcher. +character.slot.head=head +character.slot.torso=torso +character.slot.arms=arms +character.slot.hands=hands +character.slot.legs=legs +character.slot.feet=feet +character.menu.prompt=Character info. {option} +character.menu.no_options=Character info. No options. +character.pet.no_pet=No pet. +character.pet.abandon_confirm=Really abandon your pet? {option} +character.pet.unconscious_cannot_abandon=Your {pet} is unconscious. You can't abandon it right now. +item.label.items=items +item.label.item=item +item.label.unknown=Unknown +equipment.menu.equipped_suffix= (equipped) +equipment.name.none=None +equipment.name.unknown=Unknown +equipment.name.spear=Spear +equipment.name.stone_axe=Stone Axe +equipment.name.sling=Sling +equipment.name.bow=Bow +equipment.name.skin_hat=Skin Hat +equipment.name.skin_gloves=Skin Gloves +equipment.name.skin_pants=Skin Pants +equipment.name.skin_tunic=Skin Tunic +equipment.name.moccasins=Moccasins +equipment.name.skin_pouch=Skin Pouch +equipment.name.backpack=Backpack +equipment.name.fishing_pole=Fishing Pole +equipment.name_plural.spears=Spears +equipment.name_plural.stone_axes=Stone Axes +equipment.name_plural.slings=Slings +equipment.name_plural.bows=Bows +equipment.name_plural.skin_hats=Skin Hats +equipment.name_plural.skin_gloves=Skin Gloves +equipment.name_plural.skin_pants=Skin Pants +equipment.name_plural.skin_tunics=Skin Tunics +equipment.name_plural.moccasins=Moccasins +equipment.name_plural.skin_pouches=Skin Pouches +equipment.name_plural.backpacks=Backpacks +equipment.name_plural.fishing_poles=Fishing Poles +equipment.name_plural.items=Items +environment.tree.fell=Tree fell! +environment.tree.fell_with_loot=Tree fell! Got {sticks} {sticks_label}, {vines} {vines_label}, and {logs} {logs_label}. +environment.tree.inventory_full=Inventory full. +crafting.menu.prompt=Crafting menu. {option} +crafting.category.weapons=Weapons +crafting.category.tools=Tools +crafting.category.materials=Materials +crafting.category.clothing=Clothing +crafting.category.buildings=Buildings +crafting.category.barricade=Barricade +crafting.category.runes=Runes +crafting.weapons.prompt=Weapons. {option} +crafting.weapons.option.spear=Spear (1 Stick, 1 Vine, 1 Stone) [Requires Knife] +crafting.weapons.option.sling=Sling (1 Skin, 2 Vines) +crafting.weapons.option.bow=Bow (1 Stick, 1 Bowstring) +crafting.tools.prompt=Tools. {option} +crafting.tools.option.stone_knife=Stone Knife (2 Stones) +crafting.tools.option.snare=Snare (1 Stick, 2 Vines) +crafting.tools.option.stone_axe=Stone Axe (1 Stick, 1 Vine, 2 Stones) [Requires Knife] +crafting.tools.option.fishing_pole=Fishing Pole (1 Stick, 2 Vines) +crafting.tools.option.rope=Rope (3 Vines) +crafting.tools.option.quiver=Quiver (2 Skins, 2 Vines) +crafting.tools.option.canoe=Canoe (4 Logs, 11 Sticks, 11 Vines, 6 Skins, 2 Rope, 6 Reeds) +crafting.tools.option.reed_basket=Reed Basket (3 Reeds) +crafting.tools.option.clay_pot=Clay Pot (3 Clay) +crafting.materials.prompt=Materials. {option} +crafting.materials.option.butcher_game=Butcher Game [Requires Game, Knife, Fire nearby] +crafting.materials.option.smoke_fish=Smoke Fish (1 Fish, 1 Stick) [Requires Fire nearby] +crafting.materials.option.arrows=Arrows (2 Sticks, 4 Feathers, 2 Stones) [Requires Quiver] +crafting.materials.option.bowstring=Bowstring (3 Sinew) [Requires Fire nearby] +crafting.materials.option.incense=Incense (6 Sticks, 2 Vines, 1 Reed) [Requires Altar] +crafting.clothing.prompt=Clothing. {option} +crafting.clothing.option.skin_hat=Skin Hat (1 Skin, 1 Vine) +crafting.clothing.option.skin_gloves=Skin Gloves (1 Skin, 1 Vine) +crafting.clothing.option.skin_pants=Skin Pants (6 Skins, 3 Vines) +crafting.clothing.option.skin_tunic=Skin Tunic (4 Skins, 2 Vines) +crafting.clothing.option.moccasins=Moccasins (2 Skins, 1 Vine) +crafting.clothing.option.skin_pouch=Skin Pouch (2 Skins, 1 Vine) +crafting.clothing.option.backpack=Backpack (11 Skins, 5 Vines, 4 Skin Pouches) +crafting.buildings.prompt=Buildings. {option} +crafting.buildings.option.firepit=Firepit (9 Stones) +crafting.buildings.option.fire=Fire (2 Sticks, 1 Log) [Requires Firepit] +crafting.buildings.option.herb_garden=Herb Garden (9 Stones, 3 Vines, 2 Logs) [Base Only] +crafting.buildings.option.storage_upgrade_1=Upgrade Storage (6 Logs, 9 Stones, 8 Vines) [Base Only, 50 each] +crafting.buildings.option.storage_upgrade_2=Upgrade Storage (12 Logs, 18 Stones, 16 Vines) [Base Only, 100 each] +crafting.buildings.option.pasture=Pasture (8 Logs, 18 Ropes) [Base Only, Requires Storage Upgrade] +crafting.buildings.option.stable=Stable (10 Logs, 15 Stones, 10 Vines) [Base Only, Requires Storage Upgrade] +crafting.buildings.option.altar=Altar (9 Stones, 3 Sticks) [Base Only] +crafting.barricade.prompt=Barricade. {option} +crafting.barricade.option.reinforce_sticks=Reinforce with sticks ({cost} sticks, +{health} health) +crafting.barricade.option.reinforce_vines=Reinforce with vines ({cost} vines, +{health} health) +crafting.barricade.option.reinforce_log=Reinforce with log ({cost} log, +{health} health) +crafting.barricade.option.reinforce_stones=Reinforce with stones ({cost} stones, +{health} health) +crafting.missing=Missing: {requirements} +crafting.requirement.stone_knife=Stone Knife +crafting.requirement.game=Game +crafting.requirement.favor=favor +crafting.requirement.resources=resources + +[messages] +; seed:learn_sounds_label:sounds/quests/pit.ogg +msg.00884428322b=pit +; seed:learn_sounds_label:sounds/actions/break_snare.ogg +msg.01a159425acf=break snare +; src/fylgja_system.nvgt:128:insert_last[0] +msg.01af99b118c8={arg1} Fylgja +; src/item_registry.nvgt:item_plural:backpacks +msg.03268c7d7239=backpacks +; src/item_registry.nvgt:item_singular:rope +msg.03976920927e=rope +; src/menus/action_menu.nvgt:286:speak_with_history[0] +; src/menus/altar_menu.nvgt:115:speak_with_history[0] +; src/menus/base_info.nvgt:84:speak_with_history[0] +msg.03ff0c460a1f=No options. +; src/item_registry.nvgt:item_display:small_game +msg.0406529811c7=Small Game +; draugnorak.nvgt:587:ui_info_box[2] +msg.041d8413712d=A crash log was written to your save data folder. +; seed:learn_sounds_label:sounds/enemies/vampyr1.ogg +msg.04a8916a9494=vampyr1 +; src/item_registry.nvgt:item_plural:incense +msg.0532db2192fa=incense +; src/base_system.nvgt:902:notify[0] +msg.05c95b66bd57=A resident's knife broke while butchering. +; src/bosses/bandit_hideout.nvgt:783:insert_last[0] +; src/bosses/unicorn/unicorn_boss.nvgt:697:insert_last[0] +msg.066723a92e94==== Victory Rewards === +; src/crafting/craft_materials.nvgt:483:speak_with_history[0] +msg.076a11174ca0=No space for outputs. +; src/quests/enchanted_melody_game.nvgt:36:insert_last[0] +msg.07e9ab246cdd=After closing this screen, you can practice the notes. +; src/item_registry.nvgt:item_plural:sinew +msg.08b42842d5bc=sinew +; src/bosses/unicorn/unicorn_boss.nvgt:706:insert_last[0] +msg.093440631290=You have already mastered the Rune of Swiftness. +; src/menus/altar_menu.nvgt:31:speak_with_history[0] +; src/menus/altar_menu.nvgt:59:speak_with_history[0] +msg.095b4ad6f082=Runed items cannot be sacrificed. +; excluded_sounds.nvgt:34:learn_sounds_add_description[1] +msg.096539f23463=The war drums pound! Prepare for combat! +; src/item_registry.nvgt:item_display:stones +msg.09c28ae8f83b=Stones +; src/menus/base_info.nvgt:54:insert_last[0] +msg.09d016686864=Stable not built +; src/crafting/craft_tools.nvgt:205:speak_with_history[0] +msg.0a133b0ecad2=Crafted a Fishing Pole. +; src/item_registry.nvgt:item_display:reed_baskets +msg.0a3bdcef1906=Reed Baskets +; src/quests/enchanted_melody_game.nvgt:26:insert_last[0] +msg.0a9d6438c818==== Enchanted Melody === +; src/menus/action_menu.nvgt:210:speak_with_history[0] +msg.0addf2ff4572=Burned {arg1} incense. +{arg2} hours. +; src/bosses/bandit_hideout.nvgt:305:insert_last[0] +msg.0b3d8d5cdca6=- Reach the base and destroy the barricade +; src/crafting/craft_buildings.nvgt:301:speak_with_history[0] +msg.0b78c6a3e407=Pasture must be built in the base. +; src/pet_system.nvgt:570:speak_with_history[0] +msg.0ba82f2cdb6c=Your pet is on its way. +; seed:learn_sounds_label:sounds/terrain/forest.ogg +msg.0bcd9af79f2d=forest +; seed:learn_sounds_label:sounds/crafting_complete.ogg +msg.0c7789cf9a52=crafting complete +; src/menus/action_menu.nvgt:233:insert_last[0] +msg.0c976639507d=Feed fire with stick +; src/crafting/craft_weapons.nvgt:265:speak_with_history[0] +msg.0cb01a9fb609=Crafted a Stone Axe. +; src/item_registry.nvgt:item_display:arrows +msg.0cf604cb001b=Arrows +; seed:learn_sounds_label:sounds/weapons/sling_swing.ogg +msg.0d1ec2e04286=sling swing +; src/crafting/craft_clothing.nvgt:481:speak_with_history[0] +msg.0da18cb6fbe2=Crafted a Backpack. +; src/item_registry.nvgt:item_plural:knives +msg.0e16a11cda9a=knives +; src/crafting/craft_buildings.nvgt:255:speak_with_history[0] +msg.0e2e63241760=Storage must be built in the base. +; src/item_registry.nvgt:item_singular:spear +msg.0e57fb642209=spear +; src/item_registry.nvgt:item_display:slings +msg.0eb796e65c39=Slings +; src/bosses/adventure_system.nvgt:34:insert_last[0] +msg.0f5211918a6f=Unicorn Hunt (Mountain Boss) +; src/item_registry.nvgt:item_singular:skin_pouch +msg.0f9af12cbb82=skin pouch +; src/menus/action_menu.nvgt:228:insert_last[0] +msg.10065b4cd62c=Place Snare +; src/menus/action_menu.nvgt:83:speak_with_history[0] +msg.10adaed44a3e=There is already a snare here. +; src/menus/action_menu.nvgt:99:speak_with_history[0] +msg.114e55e657be=You dump an arm load of sticks into the fire. +; src/crafting/craft_buildings.nvgt:223:speak_with_history[0] +msg.118e74882896=Herb garden can only be built in the base area. +; src/crafting/craft_barricade.nvgt:127:speak_with_history[0] +msg.125769e87318=Not enough vines. +; src/item_registry.nvgt:item_display:logs +msg.126dd3b70a5c=Logs +; src/menus/action_menu.nvgt:214:speak_with_history[0] +msg.131bd02673e5=Action menu. +; libstorm-nvgt/file_viewer.nvgt:74:screen_reader_speak[0] +msg.136e4cb03bd5=Failed to save file +; src/menus/action_menu.nvgt:170:speak_with_history[0] +msg.13cd01ceccd3=Dumped {arg1} vines into the fire. +; src/pet_system.nvgt:1252:notify[0] +msg.14056a0402ef=Your {arg1} has recovered from its injuries. +; src/menus/action_menu.nvgt:43:speak_with_history[0] +msg.14272bbf4f20=No fire nearby. +; src/item_registry.nvgt:item_plural:quivers +msg.145ba8a7a0f2=quivers +; src/bosses/bandit_hideout.nvgt:856:insert_last[0] +msg.14f5f5ff4f76=No survivors were found. +; src/crafting/craft_materials.nvgt:78:speak_with_history[0] +; src/crafting/craft_materials.nvgt:112:speak_with_history[0] +msg.15e5059dafbc=Not enough quiver capacity for arrows. +; seed:learn_sounds_label:sounds/game/game_falls.ogg +msg.16134a4250f7=game falls +; src/bosses/unicorn/unicorn_boss.nvgt:711:insert_last[0] +msg.162bb07757cf=Stable or storage not built. No horses were captured. +; src/quests/escape_from_hel_game.nvgt:12:insert_last[0] +msg.16a233f44489=- Listen for open graves approaching (growing louder) +; src/crafting/craft_barricade.nvgt:200:speak_with_history[0] +msg.16cd07c0a9b6=Reinforced barricade {arg1} times with sticks. Health now {arg2}. +; src/inventory_items.nvgt:475:speak_with_history[0] +; src/inventory_items.nvgt:498:speak_with_history[0] +; src/menus/equipment_menu.nvgt:242:speak_with_history[0] +msg.16d29946de8e={arg1} equipped. +; src/world/world_fires.nvgt:38:notify[0] +msg.17213930ddb9=Fire at x {arg1} y {arg2} is getting low! +; draugnorak.nvgt:247:ui_question[1] +; seed:manual +msg.176b9a0c8398=Really exit? +; src/item_registry.nvgt:item_singular:incense_stick +msg.17808e3775e1=incense stick +; src/crafting/craft_runes.nvgt:227:speak_with_history[0] +msg.1792abbb7eb8=Select equipment to engrave with {arg1}. {arg2} +; src/quests/skeletal_bard_game.nvgt:42:insert_last[0] +msg.17b5b2a286cc==== Skeletal Bard === +; src/quests/catch_the_boomerang_game.nvgt:30:speak_with_history[0] +msg.17c677526bd6=Press Space to throw. +; seed:learn_sounds_label:sounds/quests/bat1.ogg +msg.18b837bb6f43=bat1 +; seed:learn_sounds_label:sounds/enemies/yeti_dies.ogg +msg.18c77e824a2a=yeti dies +; src/crafting/craft_materials.nvgt:423:speak_with_history[0] +msg.1a25b0443252=Butchered boar. Got meat, 3 skins, and 2 sinew. +; seed:learn_sounds_label:sounds/enemies/bandit_dies.ogg +msg.1a853cbfbc36=bandit dies +; src/crafting/crafting_core.nvgt:206:speak_with_history[0] +msg.1af5fdafa634=Crafting... +; excluded_sounds.nvgt:48:learn_sounds_add_description[1] +msg.1bb8ef2886a3=Basic undead. +; seed:learn_sounds_label:sounds/nature/night.ogg +msg.1be2a44cb53d=night +; src/bosses/bandit_hideout.nvgt:299:insert_last[0] +msg.1bfa6bc809d4==== Bandit's Hideout === +; src/quests/escape_from_hel_game.nvgt:119:speak_with_history[0] +msg.1c2272bc0140=You fell in. Score {arg1}. +; src/crafting/craft_barricade.nvgt:163:speak_with_history[0] +msg.1c5cbc1424ec=Not enough stones. +; src/item_registry.nvgt:item_singular:skin_glove +msg.1c7022965d1a=skin glove +; src/menus/base_info.nvgt:20:insert_last[0] +msg.1d15d0d5c98d=Storage built. Total items {arg1} +; src/menus/action_menu.nvgt:160:speak_with_history[0] +msg.1d493d7011a0=No vines to feed fire. +; src/bosses/bandit_hideout.nvgt:802:insert_last[0] +msg.1d61343b9660=Storage not built. No item rewards. +; seed:learn_sounds_label:sounds/items/fire.ogg +msg.1da840244989=fire +; src/item_registry.nvgt:item_singular:sling +msg.1db76ac85135=sling +; src/pet_system.nvgt:484:speak_with_history[0] +msg.20a5cf945886=Your {arg1} is unconscious. You can't abandon it right now. +; src/world/world_snares.nvgt:81:notify[0] +msg.21192381c66d=A {arg1} escaped from your snare at x {arg2} y 0! +; src/quest_system.nvgt:178:text_reader[1] +msg.21b534113694=Quest Rewards +; src/menus/action_menu.nvgt:260:insert_last[0] +msg.226f7d1cdeb1=Check fishing pole +; src/item_registry.nvgt:item_singular:axe +msg.237b5017397b=axe +; src/time_system.nvgt:658:notify[0] +msg.244b7f948d5d=The {arg1} invasion has ended. +; src/bosses/adventure_system.nvgt:21:speak_with_history[0] +msg.2455422b697c=You have already attempted an adventure today. +; src/bosses/unicorn/unicorn_boss.nvgt:117:insert_last[0] +msg.245c89590526=- When the bridge collapses with the Unicorn on it, you win! +; draugnorak.nvgt:296:speak_with_history[0] +; src/menus/menu_utils.nvgt:53:speak_with_history[0] +msg.247e80c6393a=You have died. +; src/environment.nvgt:980:speak_with_history[0] +msg.24d4bced9a39=Climbing down. +; draugnorak.nvgt:425:speak_with_history[0] +; src/bosses/bandit_hideout.nvgt:400:speak_with_history[0] +; src/bosses/unicorn/unicorn_boss.nvgt:231:speak_with_history[0] +msg.25038d9da464=east +; src/bosses/bandit_hideout.nvgt:780:speak_with_history[0] +; src/bosses/unicorn/unicorn_boss.nvgt:682:speak_with_history[0] +msg.25296a531b53=Victory! +; src/bosses/bandit_hideout.nvgt:830:insert_last[0] +msg.25db7a4ec692=Livestock were recovered and added to your pasture. +; seed:learn_sounds_label:sounds/quests/safe_to_jump.ogg +msg.2630bd23d7d4=safe to jump +; seed:learn_sounds_label:sounds/quests/spear_hit.ogg +; seed:learn_sounds_label:sounds/weapons/spear_hit.ogg +msg.2648db667f58=spear hit +; src/save_system.nvgt:439:speak_with_history[0] +msg.26c2f4289162=Load game. Select character. Press delete to remove a save. +; seed:learn_sounds_label:sounds/nature/thunder_medium.ogg +msg.270dcc07d3ae=thunder medium +; seed:learn_sounds_label:sounds/jump.ogg +; seed:learn_sounds_label:sounds/quests/jump.ogg +msg.271e9a568c0e=jump +; src/menus/base_info.nvgt:46:insert_last[0] +msg.2727056eb878=Pasture built. Livestock {arg1} of {arg2} +; libstorm-nvgt/notifications.nvgt:141:notifications_speak[0] +; libstorm-nvgt/notifications.nvgt:161:notifications_speak[0] +; libstorm-nvgt/notifications.nvgt:181:notifications_speak[0] +msg.273d131f0c8e=No notifications. +; src/time_system.nvgt:348:notify[0] +msg.27a582f91d6b={arg1} are invading from {arg2}! +; src/quests/enchanted_melody_game.nvgt:28:insert_last[0] +msg.27bbfb11a294=Repeat the magical pattern to earn favor from the gods. +; seed:learn_sounds_label:sounds/enemies/vampyr4.ogg +msg.27e4cdae51e3=vampyr4 +; src/quests/skeletal_bard_game.nvgt:50:insert_last[0] +msg.27f1dd42841b=- Press Enter to confirm +; src/menus/base_info.nvgt:30:insert_last[0] +msg.27f5254de319=Storage not built +; src/bosses/bandit_hideout.nvgt:813:insert_last[0] +msg.27f76f96867d={arg1}: +{arg2}. +; src/bosses/unicorn/unicorn_boss.nvgt:115:insert_last[0] +msg.282b619d84bc=- Or fight the Unicorn directly (it has massive health) +; src/quests/bat_invasion_game.nvgt:13:insert_last[0] +msg.285df50e183e=- Press SPACE to throw your spear when the bat sounds centered +; src/menus/action_menu.nvgt:57:speak_with_history[0] +msg.29a2d9e8aadb=Snare holds a {arg1}. +; src/crafting/craft_clothing.nvgt:176:speak_with_history[0] +msg.29a828259ef4=Crafted {arg1} Skin Hats. +; src/crafting/craft_tools.nvgt:453:speak_with_history[0] +msg.29d1fab76b47=Crafted a reed basket. +; src/quests/bat_invasion_game.nvgt:19:text_reader_lines[1] +; src/quests/catch_the_boomerang_game.nvgt:16:text_reader_lines[1] +; src/quests/enchanted_melody_game.nvgt:39:text_reader_lines[1] +msg.29f291640516=Quest Instructions +; src/bosses/unicorn/unicorn_boss.nvgt:720:insert_last[0] +msg.2a72e1170f55=No horses were captured. +; src/quests/escape_from_hel_game.nvgt:11:insert_last[0] +msg.2afb87ae1752=- You run automatically, speed increases over time +; src/flying_creature_template.nvgt:70:speak_with_history[0] +msg.2b436c9ddbbb=Butchered duck. Got 1 meat, feathers, and down. +; src/menus/altar_menu.nvgt:16:speak_with_history[0] +msg.2b8ff62c083e=Must be in base to use altar. +; src/fylgja_system.nvgt:101:insert_last[0] +msg.2badd283a63b=You have already unlocked the {arg1} Fylgja. +; excluded_sounds.nvgt:29:learn_sounds_add_description[1] +msg.2bb11d7c39dc=An enemy is in range of your currently wielded weapon. +; seed:learn_sounds_label:sounds/bosses/unicorn/unicorn_falls.ogg +msg.2bf7b1d59f34=unicorn falls +; src/item_registry.nvgt:item_display:skin_hats +msg.2c6c80931cd1=Skin Hats +; src/crafting/craft_materials.nvgt:279:speak_with_history[0] +msg.2cd0d08a829a=Crafted {arg1} Incense. +; draugnorak.nvgt:183:speak_with_history[0] +msg.2db4abf99659=New game started. +; seed:learn_sounds_label:sounds/weapons/spear_swing.ogg +msg.2e094dd0a2ab=spear swing +; src/quests/catch_the_boomerang_game.nvgt:90:speak_with_history[0] +msg.2efa88864653=The boomerang hit you.{arg1} +; src/item_registry.nvgt:item_display:fish +msg.3030e8ec7633=Fish +; src/crafting/craft_materials.nvgt:368:speak_with_history[0] +msg.30680e90d434=Smoked {arg1} fish into {arg2} smoked fish. +; src/crafting/craft_materials.nvgt:71:speak_with_history[0] +; src/crafting/craft_materials.nvgt:104:speak_with_history[0] +msg.311f421748f9=You need a quiver to craft arrows. +; src/pet_system.nvgt:466:speak_with_history[0] +msg.31342e72b48c={arg1} leaves. +; src/crafting/craft_buildings.nvgt:305:speak_with_history[0] +msg.32b43edbf3b0=Storage must be upgraded before a pasture. +; src/crafting/craft_tools.nvgt:306:speak_with_history[0] +msg.32c8e74e0055=Crafted a Quiver. +; src/quests/skeletal_bard_game.nvgt:48:insert_last[0] +msg.33bc40d8201e=- After the tune, choose the number of notes +; src/crafting/craft_materials.nvgt:168:speak_with_history[0] +msg.33c75a645734=Crafted a bowstring. +; src/item_registry.nvgt:item_singular:quiver +msg.33c89b4bdc4e=quiver +; seed:learn_sounds_label:sounds/menu/menu_move.ogg +; seed:learn_sounds_label:sounds/menu.bak/menu_move.ogg +msg.34daac1528e0=menu move +; src/crafting/craft_buildings.nvgt:335:speak_with_history[0] +msg.34e4ec0621b4=Storage must be upgraded before a stable. +; src/item_registry.nvgt:item_singular:fishing_pole +msg.34fcedc354d9=fishing pole +; src/menus/base_info.nvgt:26:insert_last[0] +msg.363b1d2abd57=Food in storage {arg1} meat, {arg2} smoked fish, {arg3} baskets of fruits and nuts. Total {arg4}. Daily use {arg5} +; src/bosses/unicorn/unicorn_boss.nvgt:728:text_reader_lines[1] +msg.364118a099cd=Unicorn Victory +; src/fylgja_system.nvgt:91:insert_last[0] +msg.36e876dbcc17=You have a {arg1} connection with the {arg2}. +; src/item_registry.nvgt:item_plural:skins +msg.37423f3bd00f=skins +; src/menus/action_menu.nvgt:61:speak_with_history[0] +msg.37bb70fed953=Snare is set and empty. +; src/item_registry.nvgt:item_singular:vine +; seed:learn_sounds_label:sounds/items/vine.ogg +msg.37ef0562902c=vine +; src/item_registry.nvgt:item_display:incense +msg.381aea2617cb=Incense +; seed:learn_sounds_label:sounds/nature/thunder_low.ogg +msg.38a7045d078c=thunder low +; seed:learn_sounds_label:sounds/weapons/arrow_flies.ogg +msg.38eb0fe3f78a=arrow flies +; src/menus/action_menu.nvgt:59:speak_with_history[0] +msg.39268e59f7ba=Snare is set but not active yet. +; src/pet_system.nvgt:551:speak_with_history[0] +msg.3976f81bbfef=Your {arg1} is leaving. +; src/item_registry.nvgt:item_singular:piece_of_sinew +msg.39784ab5ac68=piece of sinew +; src/item_registry.nvgt:item_singular:arrow +msg.39a531d92bbe=arrow +; seed:learn_sounds_label:sounds/items/item.ogg +msg.3a7d9767b123=item +; src/crafting/craft_runes.nvgt:168:speak_with_history[0] +msg.3ba80d6f0cfb=Runes. {arg1} +; src/environment.nvgt:930:speak_with_history[0] +msg.3bc5191d2dc2=Started climbing tree. Height is {arg1} feet. +; src/item_registry.nvgt:item_plural:skin_pouches +msg.3c32451c9c71=skin pouches +; src/bosses/unicorn/unicorn_boss.nvgt:713:insert_last[0] +msg.3ce35dadcc67=Stable is full. No horses were captured. +; seed:learn_sounds_label:sounds/actions/fishpole.ogg +msg.3cebb4c50866=fishpole +; src/crafting/craft_clothing.nvgt:233:speak_with_history[0] +msg.3dd6d1aafd1c=Crafted {arg1} Skin Gloves. +; seed:learn_sounds_label:sounds/terrain/hard_stone.ogg +msg.3e024b87e650=hard stone +; src/item_registry.nvgt:item_display:skins +msg.3e03229d39e4=Skins +; src/time_system.nvgt:836:notify[0] +msg.3e2cee02d8c1=The sky begins to brighten. +; src/crafting/craft_buildings.nvgt:100:speak_with_history[0] +msg.3e575fbcccad=No buildings available. +; src/world/world_fires.nvgt:44:notify[0] +msg.3e919b422247=Fire at x {arg1} y {arg2} has gone out. +; src/item_registry.nvgt:item_plural:skin_gloves +msg.3eb9cd52ec8d=skin gloves +; draugnorak.nvgt:229:speak_with_history[0] +; src/bosses/bandit_hideout.nvgt:322:speak_with_history[0] +; src/bosses/unicorn/unicorn_boss.nvgt:135:speak_with_history[0] +msg.3f098cc2c738=Paused. Press backspace to resume. +; src/crafting/craft_clothing.nvgt:345:speak_with_history[0] +msg.3f35ee808cb2=Crafted {arg1} Skin Tunics. +; src/quests/enchanted_melody_game.nvgt:34:insert_last[0] +msg.3f638a51cb47=E or U - Fourth note (highest pitch) +; src/environment.nvgt:852:speak_with_history[0] +msg.3fab86f11579=This tree is empty. +; src/base_system.nvgt:780:notify[0] +msg.3ffc8c72cadf=A {arg1} escaped while a resident checked the snare at x {arg2}. +; seed:learn_sounds_label:sounds/nature/thunder_high.ogg +msg.4010ceb056dd=thunder high +; seed:learn_sounds_label:sounds/quests/bone4.ogg +msg.403654dac805=bone4 +; src/item_registry.nvgt:item_plural:spears +msg.4037ea7ad431=spears +; seed:learn_sounds_label:sounds/enemies/wight1.ogg +msg.408a612a4167=wight1 +; src/crafting/craft_buildings.nvgt:364:speak_with_history[0] +msg.409251fa0d30=Altar must be built in the base. +; src/item_registry.nvgt:item_singular:reed_basket +msg.412fed81f58a=reed basket +; seed:learn_sounds_label:sounds/nature/wind_medium.ogg +msg.417705583821=wind medium +; seed:learn_sounds_label:sounds/terrain/fly.ogg +msg.41a682cadfe3=fly +; src/menus/action_menu.nvgt:67:speak_with_history[0] +msg.423b7f343fb8=Fish on the line. +; src/menus/action_menu.nvgt:140:speak_with_history[0] +msg.4248c4c1f5a9=Incense burning. {arg1} hours remaining. +; src/crafting/craft_weapons.nvgt:184:speak_with_history[0] +msg.425dbf90058e=Crafted {arg1} Slings. +; src/item_registry.nvgt:item_singular:boar_carcass +msg.4272004d80b4=boar carcass +; src/item_registry.nvgt:item_display:skin_pouches +msg.427944aa680d=Skin Pouches +; src/quests/bat_invasion_game.nvgt:14:insert_last[0] +msg.42b2620d1afa=- Accurate throws earn 2 points each +; src/quests/escape_from_hel_game.nvgt:14:insert_last[0] +msg.42da7d0b84a8=- Each successful jump earns 2 points +; libstorm-nvgt/file_viewer.nvgt:53:screen_reader_speak[0] +msg.434d81f4ecc4=Failed to open file: {arg1} +; src/crafting/craft_runes.nvgt:303:speak_with_history[0] +msg.43696d3d5fb8=Engraved {arg1}. +; seed:learn_sounds_label:sounds/quests/spear_miss.ogg +msg.437ecef821db=spear miss +; src/item_registry.nvgt:item_plural:bowstrings +msg.43821cae64c5=bowstrings +; src/menus/action_menu.nvgt:155:speak_with_history[0] +msg.44328bf27ea3=Dumped {arg1} sticks into the fire. +; seed:learn_sounds_label:sounds/actions/falling.ogg +msg.444d4678962b=falling +; src/crafting/craft_runes.nvgt:222:speak_with_history[0] +msg.4525b0152629=No equipment available to engrave. +; seed:learn_sounds_label:sounds/weapons/bow_fire.ogg +msg.4537c16badd9=bow fire +; src/item_registry.nvgt:item_plural:snares +msg.455b11a16eb7=snares +; src/bosses/bandit_hideout.nvgt:384:speak_with_history[0] +msg.45a604519c0d=The bandits cut you down. +; src/fishing.nvgt:168:speak_with_history[0] +msg.45bcf18418f1=You need to be within 2 tiles of a stream. +; seed:learn_sounds_label:sounds/terrain/draugrhaugr_step.ogg +msg.46fcdd91c99e=draugrhaugr step +; src/crafting/craft_tools.nvgt:287:speak_with_history[0] +msg.470f9d20c2b3=Crafted {arg1} Rope. +; src/item_registry.nvgt:item_plural:logs +msg.474c797713f3=logs +; src/quests/skeletal_bard_game.nvgt:100:speak_with_history[0] +msg.4798100ccc16=Skeletal Bard complete. Your score for all five tunes is {arg1}. +; src/item_registry.nvgt:item_display:snares +msg.483a17dd2a17=Snares +; src/crafting/craft_weapons.nvgt:241:speak_with_history[0] +msg.484adfaab5cd=Crafted {arg1} Bows. +; seed:learn_sounds_label:sounds/enemies/draugrhaugr1.ogg +msg.494f87e3874d=draugrhaugr1 +; src/item_registry.nvgt:item_plural:axes +msg.495f723e9fa1=axes +; seed:learn_sounds_label:sounds/terrain/gravel.ogg +msg.4a0e932d1bc5=gravel +; src/item_registry.nvgt:item_plural:skin_tunics +msg.4a17d4fabe93=skin tunics +; seed:learn_sounds_label:sounds/quests/boomerang.ogg +msg.4b9b4385b1a9=boomerang +; src/menus/base_info.nvgt:6:speak_with_history[0] +; src/quest_system.nvgt:98:speak_with_history[0] +msg.4bce12c3add1=You are not in the base. +; src/menus/storage_menu.nvgt:171:speak_with_history[0] +msg.4bd5f5ba3c71=Withdrew {arg1}. +; src/item_registry.nvgt:item_plural:stones +msg.4c0d2469f78b=stones +; src/crafting/craft_buildings.nvgt:158:speak_with_history[0] +msg.4c8578f10e74=There is already a firepit in the base. +; src/menus/action_menu.nvgt:117:speak_with_history[0] +msg.4d723065565f=You heave a log into the fire. +; src/crafting/craft_tools.nvgt:539:speak_with_history[0] +msg.4db797458510=Crafted {arg1} Clay Pots. +; src/pet_system.nvgt:555:speak_with_history[0] +msg.4e5ee9d0128e=No pet. +; src/crafting/craft_runes.nvgt:146:speak_with_history[0] +msg.4ea30f60b92f=Rune engraving can only be done in the base area. +; src/crafting/craft_buildings.nvgt:166:speak_with_history[0] +msg.4ecf016b2d3f=There is already a firepit here. +; seed:learn_sounds_label:sounds/notify.ogg +msg.4f08301dcb7b=notify +; seed:learn_sounds_label:sounds/terrain/shallow_water.ogg +msg.4f11c6797b8d=shallow water +; src/crafting/craft_buildings.nvgt:356:speak_with_history[0] +msg.4f4dcdd4262d=Stable built. +; src/item_registry.nvgt:item_display:bows +msg.4f773e9d5bf0=Bows +; seed:learn_sounds_label:sounds/game/goose.ogg +msg.4f7f358cd341=goose +; src/item_registry.nvgt:item_display:clay_pots +msg.4f99d3d5b638=Clay Pots +; src/quests/catch_the_boomerang_game.nvgt:75:speak_with_history[0] +msg.4ff23d0c3700=You missed the boomerang.{arg1} +; seed:learn_sounds_label:sounds/game/turkey.ogg +msg.5009c8e190ee=turkey +; src/quests/skeletal_bard_game.nvgt:49:insert_last[0] +msg.508e8bc0cc57=- Use Up/Down to change the number +; seed:learn_sounds_label:sounds/weapons/sling_low_range.ogg +msg.5123c22ce6a5=sling low range +; src/environment.nvgt:966:speak_with_history[0] +msg.514b1402c9f3=Safely reached the ground. +; src/environment.nvgt:954:speak_with_history[0] +msg.515f45b3270f=Reached the top at {arg1} feet. +; src/bosses/unicorn/unicorn_boss.nvgt:699:insert_last[0] +msg.52140591224d=The gods are pleased with your victory! {arg1} favor awarded. +; draugnorak.nvgt:79:insert_last[0] +msg.528fb1c4e5fb=New Game +; src/crafting/craft_runes.nvgt:284:speak_with_history[0] +; src/crafting/craft_runes.nvgt:317:speak_with_history[0] +msg.529f0f749181=No {arg1} available. +; src/menus/action_menu.nvgt:247:insert_last[0] +msg.52f1fd1916c9=Burn incense +; src/pet_system.nvgt:901:insert_last[0] +msg.5397e0583f14=Yes +; src/environment.nvgt:768:speak_with_history[0] +msg.53da80cba4ff=Collected {arg1} and snare. +; seed:learn_sounds_label:sounds/quests/squish.ogg +msg.5460f2e3c43e=squish +; seed:learn_sounds_label:sounds/actions/fishpole_break.ogg +msg.546330ee64db=fishpole break +; src/menus/action_menu.nvgt:89:speak_with_history[0] +msg.546f2ff04577=Snare set. +; draugnorak.nvgt:187:ui_info_box[0] +; draugnorak.nvgt:201:ui_info_box[0] +; draugnorak.nvgt:587:ui_info_box[0] +msg.54fc2d657ed6=Draugnorak +; seed:learn_sounds_label:sounds/enemies/vampyr3.ogg +msg.55748cf43121=vampyr3 +; src/crafting/craft_clothing.nvgt:308:speak_with_history[0] +msg.55c9d2b3e8a4=Crafted a Skin Tunic. +; src/quests/bat_invasion_game.nvgt:9:insert_last[0] +msg.5665ef30ef4d=Bats are invading! Throw your spear to defend. +; src/time_system.nvgt:337:notify[0] +msg.57b0465e575a=A mountain range has been discovered to the east! +; src/menus/action_menu.nvgt:53:speak_with_history[0] +msg.57e2db156a64=No snare nearby. +; src/menus/action_menu.nvgt:241:insert_last[0] +msg.57e457c02ba4=Feed fire with log +; src/item_registry.nvgt:item_display:reeds +msg.584227d57290=Reeds +; seed:learn_sounds_label:sounds/enemies/vampyr2.ogg +msg.5906872f9bb6=vampyr2 +; seed:manual +msg.596350ae8387=Found a reed. +; src/item_registry.nvgt:item_display:boar_carcasses +msg.599096c44c5b=Boar Carcasses +; libstorm-nvgt/learn_sounds.nvgt:361:learn_sounds_speak[0] +msg.5993f9da16fc=Learn sounds. +; src/quests/bat_invasion_game.nvgt:104:speak_with_history[0] +msg.59b74cafdedb=Bat invasion complete. Score {arg1}. +; src/pet_system.nvgt:1267:notify[0] +msg.59eab77f00cc=A {arg1} is getting hungry. +; src/crafting/craft_tools.nvgt:186:speak_with_history[0] +msg.5a7c581c0427=Crafted {arg1} Snares. +; seed:learn_sounds_label:sounds/enemies/wight_dies.ogg +msg.5b61e4982b59=wight dies +; src/item_registry.nvgt:item_display:feathers +msg.5bdb7fa2223d=Feathers +; src/time_system.nvgt:716:speak_with_history[0] +msg.5be2899acbe8=The residents' purpose fades. +; src/item_registry.nvgt:item_plural:reeds +msg.5c2434ecd90f=reeds +; src/bosses/bandit_hideout.nvgt:825:insert_last[0] +msg.5cac31311668=Pasture is full. No livestock were recovered. +; src/bosses/unicorn/unicorn_boss.nvgt:110:insert_last[0] +msg.5cc7429297c8=that can be destroyed with an axe. +; src/pet_system.nvgt:575:speak_with_history[0] +msg.5cda1b25dd32=A {arg1} is hungry and unresponsive. +; src/enemies/undead.nvgt:387:speak_with_history[0] +msg.5d1fa9e5a5ef=A resident has been taken. +; src/time_system.nvgt:282:notify[0] +msg.5de6d49b4e89=The area has expanded! New territory discovered to the east. +; seed:learn_sounds_label:sounds/actions/cast_strength.ogg +msg.5dfc869b7588=cast strength +; src/menus/action_menu.nvgt:91:speak_with_history[0] +msg.5e09b31082c5=No snares to place. +; src/time_system.nvgt:779:notify[0] +msg.5ea84b1201bb={arg1} favor grants you the eyes of an eagle. +; seed:learn_sounds_label:sounds/menu/menu_select.ogg +; seed:learn_sounds_label:sounds/menu.bak/menu_select.ogg +msg.5f3f3a85393b=menu select +; src/bosses/adventure_system.nvgt:16:speak_with_history[0] +msg.600007c39e3f=No adventures available in the base. +; src/crafting/craft_barricade.nvgt:11:speak_with_history[0] +; src/crafting/craft_barricade.nvgt:105:speak_with_history[0] +; src/crafting/craft_barricade.nvgt:123:speak_with_history[0] +msg.612e052d92d2=Barricade is already at full health. +; src/menus/action_menu.nvgt:48:speak_with_history[0] +msg.61b1ce299103={arg1}. {arg2} remaining. +; src/menus/base_info.nvgt:12:insert_last[0] +msg.623f78278373=Barricade health {arg1} of {arg2} +; src/bosses/bandit_hideout.nvgt:823:insert_last[0] +msg.62583aa87a59=Pasture or storage not built. No livestock were recovered. +; seed:learn_sounds_label:sounds/actions/climb_tree.ogg +msg.627a5b9e6322=climb tree +; src/crafting/craft_weapons.nvgt:79:speak_with_history[0] +msg.62b1e8703ede=Crafted a Spear. +; seed:learn_sounds_label:sounds/pets/hawk.ogg +msg.62fac2f438bb=hawk +; src/item_registry.nvgt:item_plural:fish +; src/item_registry.nvgt:item_singular:fish +msg.64875fcccaac=fish +; src/menus/base_info.nvgt:43:insert_last[0] +msg.64b945d9072e=Herb garden not built +; src/quests/catch_the_boomerang_game.nvgt:100:speak_with_history[0] +msg.656dac78b857=You caught {arg1} boomerangs for a total of {arg2} points. +; src/item_registry.nvgt:item_plural:skin_pants +; src/item_registry.nvgt:item_singular:skin_pants +msg.65eb0fb6ab40=skin pants +; src/quest_system.nvgt:102:speak_with_history[0] +msg.666d7125f17b=No quests available. +; src/item_registry.nvgt:item_plural:vines +msg.66fd9a7e91f8=vines +; src/menus/base_info.nvgt:41:insert_last[0] +msg.67ae077e6458=Herb garden built +; seed:learn_sounds_label:sounds/quests/bone3.ogg +msg.686021c25738=bone3 +; src/quests/catch_the_boomerang_game.nvgt:11:insert_last[0] +msg.6861fd95f4f7=- There are 5 turns +; src/menus/storage_menu.nvgt:67:speak_with_history[0] +msg.6a26c7cfd66b=Deposited {arg1}. +; src/quests/catch_the_boomerang_game.nvgt:71:speak_with_history[0] +msg.6a33218f6325=You {arg1} the boomerang. {arg2} points.{arg3} +; seed:learn_sounds_label:sounds/weapons/axe_swing.ogg +msg.6c265b98505c=axe swing +; libstorm-nvgt/learn_sounds.nvgt:405:learn_sounds_speak[0] +msg.6cbfe49b4adf=Unable to load sound. +; src/item_registry.nvgt:item_display:canoes +msg.6cf4935744c0=Canoes +; seed:learn_sounds_label:sounds/bosses/bandit/base_destroyed.ogg +msg.6d18e60e7952=base destroyed +; libstorm-nvgt/file_viewer.nvgt:70:screen_reader_speak[0] +msg.6d4a0243dbfd=File saved successfully +; src/item_registry.nvgt:item_plural:clay_pots +msg.6d9c2a31fbbc=clay pots +; src/quests/enchanted_melody_game.nvgt:37:insert_last[0] +msg.6e247a17f21a=Press Enter when ready to begin, or Escape to cancel. +; src/item_registry.nvgt:item_singular:knife +msg.6e32618ee300=knife +; src/bosses/unicorn/unicorn_boss.nvgt:704:insert_last[0] +msg.6fdc01a81c89=You can now engrave equipment with this rune at the crafting menu. +; seed:learn_sounds_label:sounds/actions/hit_ground.ogg +msg.704e634dd69f=hit ground +; src/crafting/craft_tools.nvgt:344:speak_with_history[0] +msg.708019666e3c=Crafted {arg1} Quivers. +; src/menus/action_menu.nvgt:124:speak_with_history[0] +; src/menus/action_menu.nvgt:190:speak_with_history[0] +; src/menus/altar_menu.nvgt:22:speak_with_history[0] +msg.70a5f57b592d=No altar built. +; seed:learn_sounds_label:sounds/actions/set_snare.ogg +msg.714165bec103=set snare +; src/quests/skeletal_bard_game.nvgt:51:insert_last[0] +msg.717d7fcc68b7=- There are 5 rounds, up to 4 points each +; src/item_registry.nvgt:item_plural:skin_hats +msg.71aad5f523eb=skin hats +; libstorm-nvgt/learn_sounds.nvgt:375:learn_sounds_speak[0] +; src/bosses/adventure_system.nvgt:60:speak_with_history[0] +; src/crafting/craft_barricade.nvgt:53:speak_with_history[0] +msg.731152a028c6=Closed. +; src/base_system.nvgt:1084:speak_with_history[0] +msg.757117781964=Residents gathered {arg1} baskets of fruits and nuts. +; src/quests/catch_the_boomerang_game.nvgt:10:insert_last[0] +msg.7680f62917b1=- Press Space again when it sounds close +; src/menus/action_menu.nvgt:132:speak_with_history[0] +; src/menus/action_menu.nvgt:198:speak_with_history[0] +msg.76c1490aad8c=No incense to burn. +; src/item_registry.nvgt:item_plural:down +; src/item_registry.nvgt:item_singular:down +msg.77346d0447da=down +; seed:learn_sounds_label:sounds/terrain/deep_forest.ogg +msg.7748b68d141a=deep forest +; src/crafting/craft_materials.nvgt:418:speak_with_history[0] +msg.778a4ff05e50=Butchered turkey. Got 1 meat and feathers. +; src/crafting/craft_buildings.nvgt:368:speak_with_history[0] +msg.7822e0af08c8=Altar already built. +; src/bosses/unicorn/unicorn_boss.nvgt:107:insert_last[0] +msg.78f036ea9ebd=The beast charges back and forth relentlessly. +; seed:learn_sounds_label:sounds/crafting.ogg +msg.7921110eb7e1=crafting +; src/item_registry.nvgt:item_plural:fishing_poles +msg.7968afd0c044=fishing poles +; src/menus/action_menu.nvgt:128:speak_with_history[0] +; src/menus/action_menu.nvgt:194:speak_with_history[0] +msg.7a61d6b17282=You need a clay pot to burn incense. +; src/crafting/craft_tools.nvgt:481:speak_with_history[0] +msg.7b50bfc0ef4b=Crafted {arg1} Reed Baskets. +; src/item_registry.nvgt:item_singular:log +msg.7babc233de26=log +; src/item_registry.nvgt:item_plural:arrows +msg.7c49f713a0fb=arrows +; seed:manual +msg.7c57f16628c7=Found clay. +; src/environment.nvgt:1136:speak_with_history[0] +msg.7cc940c217a7=Climbing {arg1}. {arg2} feet. +; src/quests/skeletal_bard_game.nvgt:45:insert_last[0] +msg.7d2cf35450e3=Listen to the melody and count the notes. +; seed:learn_sounds_label:sounds/enemies/zombie1.ogg +msg.7d88f361f3e8=zombie1 +; seed:learn_sounds_label:sounds/enemies/bandit_female_dies.ogg +msg.7e83de91b4a2=bandit female dies +; src/crafting/craft_weapons.nvgt:312:speak_with_history[0] +msg.7f6990bc78a8=Crafted {arg1} Stone Axes. +; excluded_sounds.nvgt:31:learn_sounds_add_description[1] +msg.7f7e3bbf8864=An enemy is no longer in range of your currently wielded weapon. +; src/item_registry.nvgt:item_display:backpacks +msg.7fae743693f4=Backpacks +; draugnorak.nvgt:187:ui_info_box[1] +; draugnorak.nvgt:201:ui_info_box[1] +msg.802aa6576577=Load Game +; src/item_registry.nvgt:item_plural:sticks +msg.803e4c41bbd2=sticks +; seed:learn_sounds_label:sounds/environment/tree.ogg +; seed:learn_sounds_label:sounds/items/tree.ogg +msg.80655da8d80a=tree +; src/item_registry.nvgt:item_plural:canoes +msg.806baaf8e039=canoes +; src/pet_system.nvgt:902:insert_last[0] +msg.816c52fd2bdd=No +; src/time_system.nvgt:761:notify[0] +; src/time_system.nvgt:771:notify[0] +msg.81df04a81e24={arg1} favor shines upon you. {arg2} +; src/quests/catch_the_boomerang_game.nvgt:12:insert_last[0] +msg.823be948275f=- Better timing earns more points (up to 4) +; src/menus/action_menu.nvgt:185:speak_with_history[0] +msg.8257ec29507d=Dumped {arg1} logs into the fire. +; src/crafting/craft_barricade.nvgt:286:speak_with_history[0] +msg.837affd1b6ba=Reinforced barricade {arg1} times with stones. Health now {arg2}. +; excluded_sounds.nvgt:41:learn_sounds_add_description[1] +msg.83865f248682=More powerful undead. They do not lose track of you when you are in the base. +; src/crafting/craft_tools.nvgt:102:speak_with_history[0] +msg.83a5c4369598=Crafted a Stone Knife. +; src/menus/equipment_menu.nvgt:221:speak_with_history[0] +msg.83fa126f6e3f={arg1} set to slot {arg2}. +; src/fylgja_system.nvgt:137:speak_with_history[0] +msg.8420d3438dd8=Fylgja menu. +; seed:learn_sounds_label:sounds/nature/wind_high.ogg +msg.8443de859917=wind high +; src/quests/escape_from_hel_game.nvgt:7:insert_last[0] +msg.849a4f4a399d=You are fleeing from the realm of the dead! +; src/crafting/craft_tools.nvgt:129:speak_with_history[0] +msg.84c5ff34e186=Crafted {arg1} Stone Knives. +; src/time_system.nvgt:932:notify[0] +msg.8551c25cde92=The incense has burned out. +; src/bosses/bandit_hideout.nvgt:359:speak_with_history[0] +msg.8569a552d825=x {arg1}, terrain {arg2}. Base {arg3} tiles east. Barricade {arg4} of {arg5}. +; seed:learn_sounds_label:sounds/quests/bone2.ogg +msg.85a177a79f1d=bone2 +; src/item_registry.nvgt:item_singular:feather +msg.85f15bc57d6c=feather +; src/item_registry.nvgt:item_singular:canoe +msg.862021235188=canoe +; src/base_system.nvgt:147:notify[0] +msg.862614facb79=No food, residents are hungry. +; src/crafting/craft_clothing.nvgt:195:speak_with_history[0] +msg.8626f8c56627=Crafted Skin Gloves. +; seed:learn_sounds_label:sounds/quests/bone5.ogg +msg.86290fe7e711=bone5 +; libstorm-nvgt/notifications.nvgt:148:notifications_speak[0] +msg.86e257c3efcb=Oldest notification. {arg1} +; src/item_registry.nvgt:item_display:bowstrings +msg.87383ce4344d=Bowstrings +; src/item_registry.nvgt:item_plural:ropes +msg.87cdb5c91437=ropes +; src/crafting/craft_buildings.nvgt:382:speak_with_history[0] +msg.87ee697acb01=Altar built. +; src/time_system.nvgt:775:notify[0] +msg.8818f1d55166={arg1} radiance fills residents with purpose. +; src/bosses/bandit_hideout.nvgt:340:speak_with_history[0] +msg.885cda3d707c=You flee the hideout. +; src/quests/bat_invasion_game.nvgt:7:insert_last[0] +msg.88749dbbbf09==== Bat Invasion === +; src/bosses/unicorn/unicorn_boss.nvgt:114:insert_last[0] +msg.88a8bee5206e=- Lure the Unicorn onto the bridge +; src/menus/action_menu.nvgt:256:insert_last[0] +msg.8aab96177c7d=Check snare +; src/environment.nvgt:907:speak_with_history[0] +msg.8b9881d57a1a=This area has nothing left. +; src/quests/enchanted_melody_game.nvgt:99:speak_with_history[0] +msg.8c3fa20e31d0=You matched {arg1} notes. Score {arg2}. +; src/save_system.nvgt:476:speak_with_history[0] +msg.8d0ad3cf8648=Save deleted. +; src/fishing.nvgt:354:speak_with_history[0] +; src/fishing.nvgt:382:speak_with_history[0] +msg.8d6c04ec96c7=The fish is still on the line. +; src/inventory_items.nvgt:481:speak_with_history[0] +; src/menus/equipment_menu.nvgt:237:speak_with_history[0] +msg.8e19643ce0e6={arg1} unequipped. +; src/item_registry.nvgt:item_plural:moccasins +msg.8ee83610f314=moccasins +; src/item_registry.nvgt:item_display:skin_pants +msg.8f1379e43b0f=Skin Pants +; src/quests/catch_the_boomerang_game.nvgt:6:insert_last[0] +msg.8f69276b9332=Throw a boomerang and catch it on the return. +; src/bosses/unicorn/unicorn_boss.nvgt:106:insert_last[0] +msg.8f993ac20023=You enter a narrow mountain pass. A massive Unicorn blocks your path! +; src/menus/base_info.nvgt:64:speak_with_history[0] +msg.8feaa2e73a19=Base info. {arg1} +; src/crafting/craft_barricade.nvgt:109:speak_with_history[0] +msg.9007bd6e52db=Not enough sticks. +; src/menus/altar_menu.nvgt:87:speak_with_history[0] +msg.90c991ce4eec=Altar. Favor {arg1}. +; seed:learn_sounds_label:sounds/quests/bee.ogg +msg.917ee46db0cd=bee +; src/crafting/craft_barricade.nvgt:229:speak_with_history[0] +msg.922b0aa0ef1a=Reinforced barricade {arg1} times with vines. Health now {arg2}. +; seed:learn_sounds_label:sounds/player_male_damage.ogg +msg.924e0eb6b6f1=player male damage +; src/time_system.nvgt:766:notify[0] +msg.9281d2fe5f08={arg1} favor shines upon you. You feel swift for a while. +; src/bosses/unicorn/unicorn_boss.nvgt:718:insert_last[0] +msg.92849349ef1f=A horse joins your stable. +; seed:learn_sounds_label:sounds/actions/climb_rope.ogg +msg.92abc6d6953f=climb rope +; src/fylgja_system.nvgt:99:insert_last[0] +msg.92b702e15649=You have unlocked the {arg1} Fylgja! +; src/crafting/craft_materials.nvgt:230:speak_with_history[0] +msg.93682970720f=Crafted incense. +; src/bosses/bandit_hideout.nvgt:796:insert_last[0] +msg.9379874d592b=Altar not built. No favor awarded. +; src/crafting/craft_clothing.nvgt:402:speak_with_history[0] +msg.93852d36202b=Crafted {arg1} Moccasins. +; src/bosses/unicorn/unicorn_boss.nvgt:113:insert_last[0] +msg.93894d088f6a=- Use your axe to destroy a bridge support +; src/crafting/craft_buildings.nvgt:229:speak_with_history[0] +msg.93fa0af97c4f=There is already an herb garden in the base. +; src/crafting/craft_runes.nvgt:358:speak_with_history[0] +msg.93fc8e4d1331=Engraved {arg1} {arg2} with {arg3}. +; src/item_registry.nvgt:item_singular:stick +; seed:learn_sounds_label:sounds/items/stick.ogg +msg.946466f6c963=stick +; src/quests/enchanted_melody_game.nvgt:42:speak_with_history[0] +msg.94c221dc0888=Practice mode. Press Enter to begin, Escape to cancel. +; seed:learn_sounds_label:sounds/game/boar.ogg +msg.957f554c342a=boar +; src/world/world_snares.nvgt:98:notify[0] +msg.9612a65234f9={arg1} caught in snare at x {arg2} y 0! +; src/crafting/craft_barricade.nvgt:42:speak_with_history[0] +msg.96901dc80a5b=No materials to reinforce the barricade. +; src/menus/action_menu.nvgt:77:speak_with_history[0] +msg.96b0a63888d1=Cannot place snares in the base area. +; src/bosses/bandit_hideout.nvgt:853:insert_last[0] +msg.97c59037a63c=A survivor joins your base. +; src/world/world_snares.nvgt:146:speak_with_history[0] +msg.97dcc91b2ee2=You stepped on your snare and broke it! +; src/bosses/adventure_system.nvgt:46:speak_with_history[0] +msg.988157738a23=No adventures found in this area. +; src/fishing.nvgt:434:speak_with_history[0] +msg.98ae12aa6a63=You stop fishing. +; src/item_registry.nvgt:item_plural:smoked_fish +; src/item_registry.nvgt:item_singular:smoked_fish +msg.995d4e06ae4e=smoked fish +; excluded_sounds.nvgt:35:learn_sounds_add_description[1] +msg.998b61086fa7=You picked up an item for which there is no specific sound. +; src/quests/catch_the_boomerang_game.nvgt:9:insert_last[0] +msg.9afdb7e61391=- Press Space to throw +; src/bosses/adventure_system.nvgt:39:insert_last[0] +; src/bosses/bandit_hideout.nvgt:861:text_reader_lines[1] +msg.9b64062b2074=Bandit's Hideout +; src/enemies/undead.nvgt:308:speak_with_history[0] +msg.9b81947e1398=Residents killed an attacking {arg1}. +; seed:learn_sounds_label:sounds/enemies/bandit4.ogg +msg.9bbcfc6c8f9d=bandit4 +; seed:learn_sounds_label:sounds/quests/bone6.ogg +msg.9c19f78d931d=bone6 +; seed:learn_sounds_label:sounds/quests/bat2.ogg +msg.9dd40321998e=bat2 +; src/menus/action_menu.nvgt:252:insert_last[0] +msg.9ea4f66d0740=Check fire +; src/bosses/unicorn/unicorn_boss.nvgt:154:speak_with_history[0] +msg.9fcdb8881bce=You flee the encounter. +; src/quests/bat_invasion_game.nvgt:11:insert_last[0] +; src/quests/catch_the_boomerang_game.nvgt:8:insert_last[0] +; src/quests/escape_from_hel_game.nvgt:10:insert_last[0] +msg.a040b931b9bb=How to play: +; src/quests/skeletal_bard_game.nvgt:85:speak_with_history[0] +msg.a0844149a18c=You entered {arg1} notes and the actual number was {arg2}. {arg3} points. Press Enter to continue. +; seed:learn_sounds_label:sounds/terrain/deep_water.ogg +msg.a153dba5e67a=deep water +; seed:learn_sounds_label:sounds/nature/day.ogg +msg.a2620cbc10f5=day +; src/crafting/craft_buildings.nvgt:178:speak_with_history[0] +msg.a294ccba07b9=Firepit built here. +; src/bosses/adventure_system.nvgt:51:speak_with_history[0] +msg.a2cc4be07e5e=Adventure Menu. +; src/item_registry.nvgt:item_plural:small_game +; src/item_registry.nvgt:item_singular:small_game +msg.a2d7f5d60d98=small game +; seed:learn_sounds_label:sounds/items/miscellaneous.ogg +msg.a2f3a6c42a8f=miscellaneous +; src/menus/base_info.nvgt:48:insert_last[0] +msg.a3347af174f3=Pasture not built +; seed:learn_sounds_label:sounds/pets/wolf.ogg +msg.a3cb738850fa=wolf +; src/item_registry.nvgt:item_singular:skin_hat +msg.a3f2439ef01a=skin hat +; seed:learn_sounds_label:sounds/quests/bone1.ogg +msg.a56be51786dc=bone1 +; excluded_sounds.nvgt:43:learn_sounds_add_description[1] +; excluded_sounds.nvgt:44:learn_sounds_add_description[1] +; excluded_sounds.nvgt:45:learn_sounds_add_description[1] +msg.a570b024087d=Ever hungry, they feed upon your residents. +; src/quests/escape_from_hel_game.nvgt:15:insert_last[0] +msg.a641f39a6317=- The run ends when you fall into a grave +; src/bosses/bandit_hideout.nvgt:794:insert_last[0] +msg.a646f0dcb735=Favor awarded: {arg1}. +; src/time_system.nvgt:830:notify[0] +msg.a6b2cd34521f=The sun is setting. +; src/fishing.nvgt:263:speak_with_history[0] +msg.a6c587a6a19d=A fish is on the line! +; src/item_registry.nvgt:item_plural:slings +msg.a6caf0c3771d=slings +; src/quests/escape_from_hel_game.nvgt:5:insert_last[0] +msg.a6e2492f34b6==== Escape from Hel === +; src/crafting/craft_buildings.nvgt:339:speak_with_history[0] +msg.a75c9ab9b807=Stable already built. +; src/quests/bat_invasion_game.nvgt:15:insert_last[0] +msg.a79dc18e77d9=- You have 10 attempts +; libstorm-nvgt/notifications.nvgt:168:notifications_speak[0] +msg.a7cb7754af6c=Newest notification. {arg1} +; src/bosses/unicorn/unicorn_boss.nvgt:116:insert_last[0] +msg.a95eab2b45da=- Jump (UP arrow) to avoid being trampled +; src/crafting/craft_clothing.nvgt:289:speak_with_history[0] +msg.a9cf87f2edd7=Crafted {arg1} Skin Pants. +; seed:learn_sounds_label:sounds/enemies/goblin1.ogg +msg.aa3a1916166d=goblin1 +; src/item_registry.nvgt:item_singular:clay_pot +msg.aa5edb7494f3=clay pot +; src/item_registry.nvgt:item_singular:heal_scroll +; seed:learn_sounds_label:sounds/actions/heal_scroll.ogg +msg.ab2244bc6027=heal scroll +; src/base_system.nvgt:806:notify[0] +msg.abd02188f443=Resident retrieved {arg1} from snare at x {arg2} y 0 and reset it. +; seed:manual +msg.abe73973cc7c=Unable to load save. +; src/quests/enchanted_melody_game.nvgt:30:insert_last[0] +msg.ac36af678d78=Controls: +; draugnorak.nvgt:234:speak_with_history[0] +; src/bosses/bandit_hideout.nvgt:327:speak_with_history[0] +; src/bosses/unicorn/unicorn_boss.nvgt:140:speak_with_history[0] +msg.ac66e08b54d7=Resumed. +; src/pet_system.nvgt:608:speak_with_history[0] +msg.ac8ccb330272=A {arg1} joins you. +; src/item_registry.nvgt:item_singular:moccasin +msg.ad34f00c46cd=moccasin +; seed:learn_sounds_label:sounds/enemies/goblin_dies.ogg +msg.ad36d64ad3a3=goblin dies +; src/time_system.nvgt:712:speak_with_history[0] +msg.ad610e155539=The speed blessing fades. +; src/crafting/craft_tools.nvgt:375:speak_with_history[0] +msg.ade25255fbc1=Crafted a Canoe. +; src/time_system.nvgt:869:speak_with_history[0] +msg.ae534dc25ad3=Residents repaired the barricade. +{arg1} health. +; src/menus/altar_menu.nvgt:36:speak_with_history[0] +; src/menus/altar_menu.nvgt:64:speak_with_history[0] +msg.af1bd00fc4be=Nothing to sacrifice. +; src/crafting/craft_tools.nvgt:437:speak_with_history[0] +msg.af8df8d37140=Crafted {arg1} Canoes. +; src/item_registry.nvgt:item_plural:heal_scrolls +msg.af93482d73f0=heal scrolls +; seed:learn_sounds_label:sounds/pets/black_cat.ogg +msg.aff5100bebc5=black cat +; src/crafting/craft_weapons.nvgt:127:speak_with_history[0] +msg.b0b80b668953=Crafted {arg1} Spears. +; src/item_registry.nvgt:item_display:sticks +msg.b0cca9f9205a=Sticks +; src/item_registry.nvgt:item_display:vines +msg.b0f6aa37f763=Vines +; src/item_registry.nvgt:item_display:moccasins +msg.b11aa393c8a8=Moccasins +; src/crafting/craft_weapons.nvgt:146:speak_with_history[0] +msg.b23d59c4bbce=Crafted a Sling. +; seed:learn_sounds_label:sounds/actions/start_fishing.ogg +msg.b31837f2fc5b=start fishing +; src/quests/bat_invasion_game.nvgt:12:insert_last[0] +msg.b32fc272dc0d=- Listen for bats flying past from left or right +; src/base_system.nvgt:200:speak_with_history[0] +msg.b38e4cff23de=Resident caught a fish and added it to storage. +; src/menus/storage_menu.nvgt:213:speak_with_history[0] +; src/menus/storage_menu.nvgt:232:speak_with_history[0] +; src/menus/storage_menu.nvgt:275:speak_with_history[0] +msg.b46513dbc687=Withdrew {arg1} {arg2}. +; src/item_registry.nvgt:item_display:skin_tunics +msg.b48d0661f0da=Skin Tunics +; src/environment.nvgt:847:speak_with_history[0] +msg.b4a2314b1d80=This tree has been cut down. +; src/quests/skeletal_bard_game.nvgt:13:speak_with_history[0] +msg.b4a9d19a4e85=Select number of notes. {arg1}. +; seed:learn_sounds_label:sounds/enemies/bandit3.ogg +msg.b5d5cc084a95=bandit3 +; src/base_system.nvgt:240:speak_with_history[0] +msg.b5f1924def3d=Resident smoked a fish into {arg1} smoked fish. +; src/crafting/craft_barricade.nvgt:170:speak_with_history[0] +msg.b66ee1230dac=Reinforced barricade with stones. +{arg1} health. Now {arg2} of {arg3}. +; seed:learn_sounds_label:sounds/actions/call_pet.ogg +msg.b73a0709297f=call pet +; src/item_registry.nvgt:item_display:knives +msg.b774102c89b5=Knives +; src/enemies/bandit.nvgt:389:speak_with_history[0] +msg.b8de1d1a935f=Residents killed an attacking bandit. +; seed:learn_sounds_label:sounds/terrain/snow.ogg +msg.b94e9f3d7e00=snow +; src/bosses/bandit_hideout.nvgt:840:insert_last[0] +msg.ba3af68702a3=Learned Rune of Destruction! +; src/item_registry.nvgt:item_display:baskets_of_fruits_and_nuts +msg.ba7d7dfe330b=Baskets of Fruits and Nuts +; src/crafting/craft_clothing.nvgt:364:speak_with_history[0] +msg.bad2fcec0edf=Crafted moccasins. +; src/item_registry.nvgt:item_singular:basket_of_fruits_and_nuts +msg.bb0ea68db3b4=basket of fruits and nuts +; src/item_registry.nvgt:item_plural:meat +; src/item_registry.nvgt:item_singular:meat +msg.bb40f75a9c60=meat +; seed:learn_sounds_label:sounds/terrain/grass.ogg +msg.bbc29b76c59b=grass +; src/crafting/craft_tools.nvgt:504:speak_with_history[0] +msg.bbfd1384acaf=Crafted a clay pot. +; src/menus/action_menu.nvgt:145:speak_with_history[0] +msg.bd4796595f1f=No sticks to feed fire. +; src/menus/action_menu.nvgt:70:speak_with_history[0] +msg.bd573ed65ffe=No fish on the line. +; seed:learn_sounds_label:sounds/weapons/arrow_hit.ogg +msg.bdebda25dad9=arrow hit +; src/crafting/craft_clothing.nvgt:421:speak_with_history[0] +msg.bf57e001639c=Crafted a Skin Pouch. +; src/item_registry.nvgt:item_display:down +msg.bf93e5ce8bc1=Down +; src/crafting/craft_barricade.nvgt:152:speak_with_history[0] +msg.bfc7e2fc2280=Reinforced barricade with log. +{arg1} health. Now {arg2} of {arg3}. +; excluded_sounds.nvgt:26:learn_sounds_add_description[1] +msg.c01409b3a728=When casting release control when over water. When catching release when sound is over player. +; src/bosses/bandit_hideout.nvgt:817:insert_last[0] +msg.c05feaca7269=No items were recovered. +; src/item_registry.nvgt:item_singular:snare +msg.c1104d82c0bd=snare +; src/inventory_items.nvgt:470:speak_with_history[0] +; src/inventory_items.nvgt:487:speak_with_history[0] +; src/inventory_items.nvgt:491:speak_with_history[0] +msg.c2ff4bc695b7=Item not available. +; src/menus/equipment_menu.nvgt:64:speak_with_history[0] +msg.c3080306943b=Equipment menu. +; src/crafting/craft_materials.nvgt:96:speak_with_history[0] +; src/crafting/craft_materials.nvgt:146:speak_with_history[0] +msg.c3567877be0e=Crafted {arg1} arrows. +; src/crafting/craft_clothing.nvgt:138:speak_with_history[0] +msg.c418fde85a0d=Crafted a Skin Hat. +; src/inventory_items.nvgt:99:speak_with_history[0] +msg.c477acc3f80b=You can only carry {arg1} arrows with your current quivers. +; excluded_sounds.nvgt:37:learn_sounds_add_description[1] +msg.c48d642737b2=Inhabits mountainous regions. +; src/item_registry.nvgt:item_display:smoked_fish +msg.c4f7e9c21601=Smoked Fish +; excluded_sounds.nvgt:23:learn_sounds_add_description[1] +msg.c50641e24962=Your cast missed the water and landed in nearby foliage where it is unlikely to attract fish. +; src/enemies/bandit.nvgt:395:notify[0] +; src/enemies/undead.nvgt:314:notify[0] +msg.c50d661da7cf=The barricade has fallen! +; seed:learn_sounds_label:sounds/quests/fall.ogg +msg.c52888225c69=fall +; src/crafting/craft_buildings.nvgt:247:speak_with_history[0] +msg.c5565f654184=Herb garden built. The base now heals faster. +; src/bosses/unicorn/unicorn_boss.nvgt:175:speak_with_history[0] +msg.c6186586864e=x {arg1}, terrain {arg2}. Unicorn facing {arg3} +; seed:learn_sounds_label:sounds/enemies/bandit1.ogg +msg.c65e5dfc5ba7=bandit1 +; src/bosses/bandit_hideout.nvgt:785:insert_last[0] +msg.c72b7844929d=Bandits defeated: {arg1}. +; seed:learn_sounds_label:sounds/terrain/stream.ogg +msg.c82e3d7279ef=stream +; src/menus/base_info.nvgt:52:insert_last[0] +msg.c86ace9b97f8=Stable built. Horses {arg1} of {arg2} +; src/base_system.nvgt:202:speak_with_history[0] +msg.c8d882f9098c=Residents caught {arg1} fish and added them to storage. +; excluded_sounds.nvgt:33:learn_sounds_add_description[1] +msg.c8da1c4f1714=Lowers in pitch as the fall progresses. +; src/pet_system.nvgt:914:speak_with_history[0] +; src/pet_system.nvgt:937:speak_with_history[0] +msg.c8fee26f6ee3=Declined. +; src/item_registry.nvgt:item_display:meat +msg.c90399f41b3f=Meat +; seed:learn_sounds_label:sounds/quests/bone7.ogg +msg.c9532ab465fb=bone7 +; src/bosses/unicorn/unicorn_boss.nvgt:703:insert_last[0] +msg.cb6c212139a1=Learned Rune of Swiftness! +; src/bosses/bandit_hideout.nvgt:804:insert_last[0] +msg.cb8b9677952a=Storage rewards: +; src/item_registry.nvgt:item_plural:boar_carcasses +msg.cb8e70ab2bcc=boar carcasses +; src/item_registry.nvgt:item_singular:bowstring +msg.cc167fb645e4=bowstring +; src/menus/action_menu.nvgt:175:speak_with_history[0] +msg.cc3caa9812e2=No logs to feed fire. +; src/item_registry.nvgt:item_plural:reed_baskets +msg.cc61d0f3de65=reed baskets +; src/crafting/craft_buildings.nvgt:188:speak_with_history[0] +msg.ccb9a6240155=You need a firepit within 2 tiles to build a fire. +; seed:learn_sounds_label:sounds/weapons/axe_hit.ogg +msg.cccc5c842adb=axe hit +; src/crafting/craft_buildings.nvgt:309:speak_with_history[0] +msg.ccce38999a27=Pasture already built. +; src/time_system.nvgt:720:speak_with_history[0] +msg.cd13e6ce9297=The eagle's sight fades. +; src/menus/storage_menu.nvgt:106:speak_with_history[0] +; src/menus/storage_menu.nvgt:126:speak_with_history[0] +; src/menus/storage_menu.nvgt:161:speak_with_history[0] +msg.cd3ef7da3f4f=Deposited {arg1} {arg2}. +; draugnorak.nvgt:277:speak_with_history[0] +; src/menus/menu_utils.nvgt:35:speak_with_history[0] +msg.cd4c1cc68057=Burning! {arg1} health remaining. +; seed:learn_sounds_label:sounds/enemies/enter_range.ogg +msg.ce13e4341cdc=enter range +; src/menus/inventory_core.nvgt:25:insert_last[0] +; src/menus/inventory_core.nvgt:41:insert_last[0] +; src/menus/storage_menu.nvgt:287:insert_last[0] +msg.ce3897c6b3be={arg1}: {arg2} +; seed:learn_sounds_label:sounds/items/item_breaks.ogg +msg.ce5aeca95fb9=item breaks +; src/i18n.nvgt:669:speak_with_history[0] +; src/pet_system.nvgt:906:speak_with_history[0] +; src/save_system.nvgt:344:speak_with_history[0] +msg.cea88b214070={arg1} {arg2} +; seed:manual +msg.cfe2650b4c6e=Found a stone. +; src/bosses/unicorn/unicorn_boss.nvgt:119:insert_last[0] +msg.d04a59a6a71b=Controls: LEFT/RIGHT to move, UP to jump, CTRL to attack, ESC to flee +; src/item_registry.nvgt:item_singular:skin +msg.d0cb2acd0739=skin +; src/menus/altar_menu.nvgt:52:speak_with_history[0] +msg.d103663bcfd9=Sacrificed 1 {arg1}. Favor +{arg2}. Total {arg3}. +; src/crafting/craft_barricade.nvgt:116:speak_with_history[0] +msg.d1c5baa8a36b=Reinforced barricade with sticks. +{arg1} health. Now {arg2} of {arg3}. +; seed:learn_sounds_label:sounds/enemies/enemy_falls.ogg +msg.d34b6217ea75=enemy falls +; src/item_registry.nvgt:item_display:skin_gloves +msg.d373510877fd=Skin Gloves +; draugnorak.nvgt:587:ui_info_box[1] +; seed:manual +msg.d39008c4a392=Unhandled exception +; src/item_registry.nvgt:item_singular:backpack +msg.d49f2e760778=backpack +; src/quests/enchanted_melody_game.nvgt:33:insert_last[0] +msg.d537941485fb=R or I - Third note +; src/crafting/craft_buildings.nvgt:214:speak_with_history[0] +msg.d5dd6ddda4ef=Fire built at firepit. +; src/menus/equipment_menu.nvgt:34:speak_with_history[0] +msg.d5e86e1918e8=Nothing to equip. +; src/crafting/craft_buildings.nvgt:331:speak_with_history[0] +msg.d5e89cddb940=Stable must be built in the base. +; draugnorak.nvgt:187:ui_info_box[2] +; seed:manual +msg.d609f615ddae=No saves found. +; draugnorak.nvgt:418:speak_with_history[0] +; src/bosses/bandit_hideout.nvgt:395:speak_with_history[0] +; src/bosses/unicorn/unicorn_boss.nvgt:226:speak_with_history[0] +msg.d63eba28afc0=west +; seed:learn_sounds_label:sounds/enemies/invasion.ogg +msg.d6dc139f529a=invasion +; seed:learn_sounds_label:sounds/enemies/yeti1.ogg +msg.d6fd3467a950=yeti1 +; src/item_registry.nvgt:item_display:axes +msg.d75f36f88797=Axes +; src/bosses/bandit_hideout.nvgt:832:insert_last[0] +msg.d7879bd506af=No livestock were recovered. +; src/time_system.nvgt:650:notify[0] +msg.d8ea835664e1=Your base is at maximum capacity. +; src/quests/enchanted_melody_game.nvgt:31:insert_last[0] +msg.da0b5a461d98=F or K - First note (lowest pitch) +; seed:learn_sounds_label:sounds/weapons/sling_hit.ogg +msg.da11dc5e090c=sling hit +; seed:learn_sounds_label:sounds/enemies/vampyr_dies.ogg +msg.da93fb9992a3=vampyr dies +; src/fishing.nvgt:331:speak_with_history[0] +msg.dadcebf86f43=Caught a {arg1} {arg2}. +; excluded_sounds.nvgt:47:learn_sounds_add_description[1] +msg.dadd68431e60=Inhabits snowy regions. +; seed:learn_sounds_label:sounds/enemies/exit_range.ogg +msg.db2a8514a0fc=exit range +; src/quest_system.nvgt:204:speak_with_history[0] +msg.db476a28fe5b=Quest menu. +; src/bosses/bandit_hideout.nvgt:309:text_reader_lines[1] +; src/bosses/unicorn/unicorn_boss.nvgt:121:text_reader_lines[1] +msg.db56e2e1db9f=Adventure +; seed:learn_sounds_label:sounds/enemies/bandit2.ogg +msg.db5bbabf1bc3=bandit2 +; draugnorak.nvgt:70:speak_with_history[0] +msg.dbea5ffbac7b=Draugnorak. Main menu. +; src/menus/action_menu.nvgt:346:speak_with_history[0] +msg.dc5c17f80fbe=Can't do that. +; src/fylgja_system.nvgt:115:speak_with_history[0] +msg.ddea499514d8=You have already used your Fylgja today. +; src/item_registry.nvgt:item_display:sinew +msg.ddf360815408=Sinew +; src/bosses/bandit_hideout.nvgt:307:insert_last[0] +msg.ddfc9c735c48=Bandits will, of course, not take this lying down. +; src/item_registry.nvgt:item_plural:baskets_of_fruits_and_nuts +msg.de0dce8ef11c=baskets of fruits and nuts +; src/menus/base_info.nvgt:38:insert_last[0] +msg.de2c51fece28=Altar not built +; seed:learn_sounds_label:sounds/quests/footstep.ogg +msg.de4e8495f978=footstep +; src/item_registry.nvgt:item_plural:clay +; src/item_registry.nvgt:item_singular:clay +msg.de6b29cb1808=clay +; src/item_registry.nvgt:item_plural:feathers +msg.de752f6fdd8b=feathers +; src/quests/skeletal_bard_game.nvgt:44:insert_last[0] +msg.df3f34f3f144=A skeleton named Billy Bones is practicing to become a bard. +; src/bosses/bandit_hideout.nvgt:304:insert_last[0] +msg.df63c8846d76=Objective: +; src/crafting/craft_clothing.nvgt:459:speak_with_history[0] +msg.df93ea08d751=Crafted {arg1} Skin Pouches. +; src/crafting/craft_clothing.nvgt:524:speak_with_history[0] +msg.dfa22c1cada0=Crafted {arg1} Backpacks. +; src/menus/altar_menu.nvgt:81:speak_with_history[0] +msg.dffaa4b11d31=Sacrificed {arg1} {arg2}. Favor +{arg3}. Total {arg4}. +; src/item_registry.nvgt:item_display:clay +msg.e00d5664a78e=Clay +; src/bosses/unicorn/unicorn_boss.nvgt:109:insert_last[0] +msg.e0b678c82a60=A wooden bridge spans a deep chasm ahead. The bridge has two supports +; src/crafting/craft_runes.nvgt:163:speak_with_history[0] +msg.e0c7f029cfa2=No runes unlocked yet. +; src/bosses/unicorn/unicorn_boss.nvgt:112:insert_last[0] +msg.e142524d7689=Strategy: +; libstorm-nvgt/learn_sounds.nvgt:363:learn_sounds_speak[0] +msg.e1c16d0490ee=No sounds available. +; src/crafting/craft_tools.nvgt:243:speak_with_history[0] +msg.e1dc1099a799=Crafted {arg1} Fishing Poles. +; src/item_registry.nvgt:item_singular:skin_tunic +msg.e253d297aabc=skin tunic +; src/item_registry.nvgt:item_display:fishing_poles +msg.e2e91797da96=Fishing Poles +; src/item_registry.nvgt:item_singular:stone +; seed:learn_sounds_label:sounds/items/stone.ogg +; seed:learn_sounds_label:sounds/terrain/stone.ogg +msg.e30bfd0c38dc=stone +; src/crafting/craft_tools.nvgt:259:speak_with_history[0] +msg.e31811cdce94=Crafted rope. +; seed:learn_sounds_label:sounds/actions/bad_cast.ogg +msg.e34161b98188=bad cast +; src/bosses/unicorn/unicorn_boss.nvgt:700:insert_last[0] +msg.e379741bfbd3=Heal Scrolls: +{arg1}. +; src/item_registry.nvgt:item_display:quivers +msg.e3ce3e543239=Quivers +; seed:learn_sounds_label:sounds/weapons/sling_high_range.ogg +msg.e3d828480491=sling high range +; src/quests/bat_invasion_game.nvgt:17:insert_last[0] +; src/quests/catch_the_boomerang_game.nvgt:14:insert_last[0] +; src/quests/escape_from_hel_game.nvgt:17:insert_last[0] +msg.e4427f09bc09=Close this screen to begin. +; src/menus/action_menu.nvgt:237:insert_last[0] +msg.e5a51ddbb159=Feed fire with vine +; seed:learn_sounds_label:sounds/enemies/undead_resident1.ogg +msg.e5d79aba2048=undead resident1 +; seed:learn_sounds_label:sounds/enemies/zombie_hit.ogg +msg.e5f656d506a7=zombie hit +; src/quests/escape_from_hel_game.nvgt:8:insert_last[0] +msg.e6526216c064=You have plundered the treasure, and the Draugr are displeased.! +; src/bosses/bandit_hideout.nvgt:843:insert_last[0] +msg.e65fa8960222=You have already mastered the Rune of Destruction. +; seed:learn_sounds_label:sounds/quests/bone8.ogg +msg.e6ae58426f13=bone8 +; seed:learn_sounds_label:sounds/actions/feed_fire.ogg +msg.e7409fcb54bc=feed fire +; src/environment.nvgt:775:speak_with_history[0] +msg.e74f453392b6=Collected snare. +; src/crafting/craft_barricade.nvgt:134:speak_with_history[0] +msg.e75d3068a5e9=Reinforced barricade with vines. +{arg1} health. Now {arg2} of {arg3}. +; seed:learn_sounds_label:sounds/terrain/wood.ogg +msg.e8002c169040=wood +; src/quests/escape_from_hel_game.nvgt:13:insert_last[0] +msg.e8216720d30b=- Press SPACE to jump over graves +; draugnorak.nvgt:89:insert_last[0] +msg.e8db3a42f86f=Learn Sounds +; src/bosses/bandit_hideout.nvgt:841:insert_last[0] +msg.e8e8f4ee7a5a=You can now engrave weapons with this rune at the crafting menu. +; src/crafting/craft_barricade.nvgt:145:speak_with_history[0] +msg.e8fe153261a9=Not enough logs. +; src/bosses/bandit_hideout.nvgt:302:insert_last[0] +msg.e9dd1d268b65=The base lies far to the east, guarded by a barricade. +; src/bosses/unicorn/unicorn_boss.nvgt:211:speak_with_history[0] +msg.ea6bfdb7bd29=The Unicorn trampled you. +; src/menus/base_info.nvgt:36:insert_last[0] +msg.ea771032e597=Altar built +; src/fishing.nvgt:161:speak_with_history[0] +msg.ead6f13efc8d=You need a fishing pole equipped. +; src/crafting/craft_tools.nvgt:148:speak_with_history[0] +msg.eb9cac43e997=Crafted a Snare. +; libstorm-nvgt/notifications.nvgt:153:notifications_speak[0] +; libstorm-nvgt/notifications.nvgt:173:notifications_speak[0] +msg.ebf229b227f8={arg1} {arg2} of {arg3} +; src/fylgja_system.nvgt:182:speak_with_history[0] +msg.ec465c0c4000=You need open ground in front of you to charge. +; src/base_system.nvgt:1082:speak_with_history[0] +msg.eced46d198ed=Resident gathered a basket of fruits and nuts. +; src/environment.nvgt:1191:speak_with_history[0] +msg.ed989350becb=Reached elevation {arg1}. +; draugnorak.nvgt:289:speak_with_history[0] +; src/menus/menu_utils.nvgt:46:speak_with_history[0] +msg.edbcc0a199d1={arg1} health. +; src/world/world_snares.nvgt:144:speak_with_history[0] +msg.ee02d6d4c199=You stepped on your snare! The {arg1} escaped. +; src/item_registry.nvgt:item_display:spears +msg.ee200b3d8ae2=Spears +; seed:learn_sounds_label:sounds/bosses/unicorn/unicorn_on_bridge.ogg +msg.eff5a5f34d9a=unicorn on bridge +; seed:learn_sounds_label:sounds/weapons/bow_draw.ogg +msg.f015f25c409b=bow draw +; src/quests/enchanted_melody_game.nvgt:75:speak_with_history[0] +msg.f08ee9fe3340=Starting. Repeat the pattern. +; src/item_registry.nvgt:item_display:ropes +msg.f12b2d219fe8=Ropes +; src/time_system.nvgt:469:speak_with_history[0] +msg.f137213e72e2=A swarm of zombies has been spotted. +; excluded_sounds.nvgt:38:learn_sounds_add_description[1] +msg.f16169ef79aa=Not quite a zombie nor a vampyr. These undead remember they used to have a home, and "\n "maybe there's food there... +; src/menus/base_info.nvgt:62:speak_with_history[0] +msg.f1c671ae3045=Base info. No options. +; src/environment.nvgt:29:speak_with_history[0] +msg.f229791ba917=Fell {arg1} feet! Took {arg2} damage. {arg3} health remaining. +; src/quests/catch_the_boomerang_game.nvgt:4:insert_last[0] +msg.f305da2c3550==== Catch the Boomerang === +; draugnorak.nvgt:334:speak_with_history[0] +msg.f32ea7a06f91={arg1}, x {arg2}, y {arg3}, terrain {arg4} +; seed:learn_sounds_label:sounds/nature/wind_low.ogg +msg.f3c5ed579a54=wind low +; seed:learn_sounds_label:sounds/player_female_damage.ogg +msg.f3d964225e83=player female damage +; src/crafting/craft_buildings.nvgt:323:speak_with_history[0] +msg.f3e51f53f970=Pasture built. +; src/crafting/craft_barricade.nvgt:258:speak_with_history[0] +msg.f40ad52f4d97=Reinforced barricade {arg1} times with log. Health now {arg2}. +; src/crafting/craft_materials.nvgt:427:speak_with_history[0] +msg.f48b940f83c7=Butchered {arg1}. Got 1 meat and 1 skin. +; src/crafting/craft_materials.nvgt:414:speak_with_history[0] +msg.f50492d2703a=Butchered goose. Got 1 meat, feathers, and down. +; src/item_registry.nvgt:item_singular:reed +msg.f5cabf873590=reed +; src/base_system.nvgt:1006:speak_with_history[0] +msg.f5f6c13f2ceb=Resident added {arg1} to storage. +; src/bosses/bandit_hideout.nvgt:301:insert_last[0] +msg.f77fc018cb7b=You find a hidden bandit base deep in the forest. +; draugnorak.nvgt:105:insert_last[0] +msg.f83b6fe3aebf=Exit +; src/inventory_items.nvgt:458:speak_with_history[0] +; src/quests/skeletal_bard_game.nvgt:24:speak_with_history[0] +; src/quests/skeletal_bard_game.nvgt:30:speak_with_history[0] +msg.f85b4b604c9b={arg1}. +; seed:learn_sounds_label:sounds/enemies/undead_resident_dies.ogg +msg.f8909d26fe86=undead resident dies +; src/crafting/craft_weapons.nvgt:203:speak_with_history[0] +msg.f8b2a45e7e4b=Crafted a Bow. +; seed:manual +msg.f8fc573e2d2f=Load Game (no saves found) +; src/item_registry.nvgt:item_singular:bow +msg.fa5bd38090c7=bow +; draugnorak.nvgt:195:speak_with_history[0] +msg.fa87b71ab25a=Game loaded. +; src/crafting/craft_buildings.nvgt:196:speak_with_history[0] +msg.fa88a4c8033c=There is already a fire in the base. +; src/crafting/craft_clothing.nvgt:252:speak_with_history[0] +msg.fb091149bb52=Crafted Skin Pants. +; src/quest_system.nvgt:72:notify[0] +msg.fb09c6d50b11=A new quest is available: {arg1}. +; src/crafting/craft_buildings.nvgt:293:speak_with_history[0] +msg.fb389469701e=Storage upgraded. Capacity is now {arg1} per item. +; seed:learn_sounds_label:sounds/bosses/unicorn/unicorn_galloping.ogg +msg.fb4a022a82b3=unicorn galloping +; seed:learn_sounds_label:sounds/nature/rain.ogg +msg.fbec17cb2fcb=rain +; src/menus/action_menu.nvgt:284:speak_with_history[0] +; src/menus/altar_menu.nvgt:113:speak_with_history[0] +; src/menus/altar_menu.nvgt:153:speak_with_history[0] +msg.fc1740dbb4ae=No matches for {arg1}. +; src/crafting/craft_runes.nvgt:216:insert_last[0] +msg.fc1ad6e10ac4={arg1} ({arg2} available) +; src/crafting/craft_materials.nvgt:203:speak_with_history[0] +msg.fc5294f9b150=Crafted {arg1} Bowstrings. +; src/item_registry.nvgt:item_display:heal_scrolls +msg.fca1af3d07b0=Heal Scrolls +; libstorm-nvgt/volume_controls.nvgt:71:screen_reader_speak[0] +msg.fd1a25749457=Volume {arg1}. +; src/menus/action_menu.nvgt:108:speak_with_history[0] +msg.fdd0b57cb7b9=You toss a fiew vines and leaves into the fire. +; src/crafting/craft_buildings.nvgt:259:speak_with_history[0] +msg.fe0ede7d0360=Storage is fully upgraded. +; seed:learn_sounds_label:sounds/menu/Draugnorak.ogg +; seed:learn_sounds_label:sounds/menu.bak/Draugnorak.ogg +msg.fe2a9d6606e0=draugnorak +; src/quests/bat_invasion_game.nvgt:21:speak_with_history[0] +msg.fe406c6e0055=Starting. +; src/bosses/unicorn/unicorn_boss.nvgt:104:insert_last[0] +msg.feea4a3b0a84==== Unicorn Hunt === +; src/quests/enchanted_melody_game.nvgt:32:insert_last[0] +msg.fefc084f67de=D or J - Second note +; src/item_registry.nvgt:item_plural:bows +msg.ff5ac0b6df7b=bows +; src/crafting/craft_materials.nvgt:308:speak_with_history[0] +msg.ffed2db61296=Smoked a fish into {arg1} smoked fish. diff --git a/lang/en.template.ini b/lang/en.template.ini new file mode 100644 index 0000000..5503da8 --- /dev/null +++ b/lang/en.template.ini @@ -0,0 +1,1531 @@ +; Draugnorak localization catalog +; Copy this file to lang/.ini and translate only the right-hand values. +; Keep keys unchanged. Use placeholders like {arg1}, {count}, {language} exactly as written. + +[meta] +code=en +name=English +native_name=English + +[system] +language.select_prompt=Select your language. +language.selected=Language set to {language}. +language.english_label=English (en) +ui.window_title=Draugnorak +sex.male=Male +sex.female=Female +new_character.choose_sex=Choose your sex. +new_character.enter_name=Enter your name or press Enter for random. +new_character.save_exists_overwrite=Save found for {name}. Overwrite? +load_game.option_with_metadata={name}, {sex}, day {day} +load_game.delete_confirm_base=Are you sure you want to delete the character {name}? +load_game.delete_confirm_with_metadata=Are you sure you want to delete the character {name} gender {sex} days {day}? +load_game.delete_save_heading=Delete Save +load_game.delete_save_failed=Unable to delete save. +quick_slot.no_item_bound=No item bound to slot {slot}. +menu.closed=Closed. +menu.canceled=Canceled. +menu.no_options=No options. +menu.no_matches=No matches for {arg1}. +option.no=No +option.yes=Yes +inventory.cant_carry_any_more_item=You can't carry any more {item}. +inventory.menu.prompt=Inventory menu. {option} +inventory.menu.no_options=Inventory menu. No options. +inventory.option.personal=Personal inventory +inventory.option.base_storage=Base storage +inventory.count_set_to_slot={name} count set to slot {slot}. +inventory.need_quiver_for_arrows=You need a quiver to carry arrows. +search.found_item=Found {item}. +search.found_nothing=Found nothing. +pickup.item=Picked up {item}. +storage.window_title=Inventory +storage.transfer_prompt={prompt} (max {max}) +storage.deposit_how_many=Deposit how many? +storage.withdraw_how_many=Withdraw how many? +storage.nothing_to_deposit=Nothing to deposit. +storage.nothing_to_withdraw=Nothing to withdraw. +storage.runed_cannot_deposit=Runed items cannot be deposited into storage. +storage.item_full=Storage for that item is full. +storage.no_storage_built=No storage built. +storage.menu_title=Base storage. +storage.menu.prompt=Base storage. {option} +storage.menu.no_options=Base storage. No options. +crafting.require.fire_within_three_clay_pot=You need a fire within 3 tiles to craft a clay pot. +crafting.require.fire_within_three_clay_pots=You need a fire within 3 tiles to craft clay pots. +crafting.require.fire_within_three_bowstring=You need a fire within 3 tiles to make bowstring. +crafting.require.altar_incense=You need an altar to craft incense. +crafting.require.fire_within_three_smoke_fish=You need a fire within 3 tiles to smoke fish. +crafting.require.fire_within_three_butcher=You need a fire within 3 tiles to butcher. +character.slot.head=head +character.slot.torso=torso +character.slot.arms=arms +character.slot.hands=hands +character.slot.legs=legs +character.slot.feet=feet +character.menu.prompt=Character info. {option} +character.menu.no_options=Character info. No options. +character.pet.no_pet=No pet. +character.pet.abandon_confirm=Really abandon your pet? {option} +character.pet.unconscious_cannot_abandon=Your {pet} is unconscious. You can't abandon it right now. +item.label.items=items +item.label.item=item +item.label.unknown=Unknown +equipment.menu.equipped_suffix= (equipped) +equipment.name.none=None +equipment.name.unknown=Unknown +equipment.name.spear=Spear +equipment.name.stone_axe=Stone Axe +equipment.name.sling=Sling +equipment.name.bow=Bow +equipment.name.skin_hat=Skin Hat +equipment.name.skin_gloves=Skin Gloves +equipment.name.skin_pants=Skin Pants +equipment.name.skin_tunic=Skin Tunic +equipment.name.moccasins=Moccasins +equipment.name.skin_pouch=Skin Pouch +equipment.name.backpack=Backpack +equipment.name.fishing_pole=Fishing Pole +equipment.name_plural.spears=Spears +equipment.name_plural.stone_axes=Stone Axes +equipment.name_plural.slings=Slings +equipment.name_plural.bows=Bows +equipment.name_plural.skin_hats=Skin Hats +equipment.name_plural.skin_gloves=Skin Gloves +equipment.name_plural.skin_pants=Skin Pants +equipment.name_plural.skin_tunics=Skin Tunics +equipment.name_plural.moccasins=Moccasins +equipment.name_plural.skin_pouches=Skin Pouches +equipment.name_plural.backpacks=Backpacks +equipment.name_plural.fishing_poles=Fishing Poles +equipment.name_plural.items=Items +environment.tree.fell=Tree fell! +environment.tree.fell_with_loot=Tree fell! Got {sticks} {sticks_label}, {vines} {vines_label}, and {logs} {logs_label}. +environment.tree.inventory_full=Inventory full. +crafting.menu.prompt=Crafting menu. {option} +crafting.category.weapons=Weapons +crafting.category.tools=Tools +crafting.category.materials=Materials +crafting.category.clothing=Clothing +crafting.category.buildings=Buildings +crafting.category.barricade=Barricade +crafting.category.runes=Runes +crafting.weapons.prompt=Weapons. {option} +crafting.weapons.option.spear=Spear (1 Stick, 1 Vine, 1 Stone) [Requires Knife] +crafting.weapons.option.sling=Sling (1 Skin, 2 Vines) +crafting.weapons.option.bow=Bow (1 Stick, 1 Bowstring) +crafting.tools.prompt=Tools. {option} +crafting.tools.option.stone_knife=Stone Knife (2 Stones) +crafting.tools.option.snare=Snare (1 Stick, 2 Vines) +crafting.tools.option.stone_axe=Stone Axe (1 Stick, 1 Vine, 2 Stones) [Requires Knife] +crafting.tools.option.fishing_pole=Fishing Pole (1 Stick, 2 Vines) +crafting.tools.option.rope=Rope (3 Vines) +crafting.tools.option.quiver=Quiver (2 Skins, 2 Vines) +crafting.tools.option.canoe=Canoe (4 Logs, 11 Sticks, 11 Vines, 6 Skins, 2 Rope, 6 Reeds) +crafting.tools.option.reed_basket=Reed Basket (3 Reeds) +crafting.tools.option.clay_pot=Clay Pot (3 Clay) +crafting.materials.prompt=Materials. {option} +crafting.materials.option.butcher_game=Butcher Game [Requires Game, Knife, Fire nearby] +crafting.materials.option.smoke_fish=Smoke Fish (1 Fish, 1 Stick) [Requires Fire nearby] +crafting.materials.option.arrows=Arrows (2 Sticks, 4 Feathers, 2 Stones) [Requires Quiver] +crafting.materials.option.bowstring=Bowstring (3 Sinew) [Requires Fire nearby] +crafting.materials.option.incense=Incense (6 Sticks, 2 Vines, 1 Reed) [Requires Altar] +crafting.clothing.prompt=Clothing. {option} +crafting.clothing.option.skin_hat=Skin Hat (1 Skin, 1 Vine) +crafting.clothing.option.skin_gloves=Skin Gloves (1 Skin, 1 Vine) +crafting.clothing.option.skin_pants=Skin Pants (6 Skins, 3 Vines) +crafting.clothing.option.skin_tunic=Skin Tunic (4 Skins, 2 Vines) +crafting.clothing.option.moccasins=Moccasins (2 Skins, 1 Vine) +crafting.clothing.option.skin_pouch=Skin Pouch (2 Skins, 1 Vine) +crafting.clothing.option.backpack=Backpack (11 Skins, 5 Vines, 4 Skin Pouches) +crafting.buildings.prompt=Buildings. {option} +crafting.buildings.option.firepit=Firepit (9 Stones) +crafting.buildings.option.fire=Fire (2 Sticks, 1 Log) [Requires Firepit] +crafting.buildings.option.herb_garden=Herb Garden (9 Stones, 3 Vines, 2 Logs) [Base Only] +crafting.buildings.option.storage_upgrade_1=Upgrade Storage (6 Logs, 9 Stones, 8 Vines) [Base Only, 50 each] +crafting.buildings.option.storage_upgrade_2=Upgrade Storage (12 Logs, 18 Stones, 16 Vines) [Base Only, 100 each] +crafting.buildings.option.pasture=Pasture (8 Logs, 18 Ropes) [Base Only, Requires Storage Upgrade] +crafting.buildings.option.stable=Stable (10 Logs, 15 Stones, 10 Vines) [Base Only, Requires Storage Upgrade] +crafting.buildings.option.altar=Altar (9 Stones, 3 Sticks) [Base Only] +crafting.barricade.prompt=Barricade. {option} +crafting.barricade.option.reinforce_sticks=Reinforce with sticks ({cost} sticks, +{health} health) +crafting.barricade.option.reinforce_vines=Reinforce with vines ({cost} vines, +{health} health) +crafting.barricade.option.reinforce_log=Reinforce with log ({cost} log, +{health} health) +crafting.barricade.option.reinforce_stones=Reinforce with stones ({cost} stones, +{health} health) +crafting.missing=Missing: {requirements} +crafting.requirement.stone_knife=Stone Knife +crafting.requirement.game=Game +crafting.requirement.favor=favor +crafting.requirement.resources=resources + +[messages] +; seed:learn_sounds_label:sounds/quests/pit.ogg +msg.00884428322b=pit +; seed:learn_sounds_label:sounds/actions/break_snare.ogg +msg.01a159425acf=break snare +; src/fylgja_system.nvgt:128:insert_last[0] +msg.01af99b118c8={arg1} Fylgja +; src/item_registry.nvgt:item_plural:backpacks +msg.03268c7d7239=backpacks +; src/item_registry.nvgt:item_singular:rope +msg.03976920927e=rope +; src/menus/action_menu.nvgt:286:speak_with_history[0] +; src/menus/altar_menu.nvgt:115:speak_with_history[0] +; src/menus/base_info.nvgt:84:speak_with_history[0] +msg.03ff0c460a1f=No options. +; src/item_registry.nvgt:item_display:small_game +msg.0406529811c7=Small Game +; draugnorak.nvgt:587:ui_info_box[2] +msg.041d8413712d=A crash log was written to your save data folder. +; seed:learn_sounds_label:sounds/enemies/vampyr1.ogg +msg.04a8916a9494=vampyr1 +; src/item_registry.nvgt:item_plural:incense +msg.0532db2192fa=incense +; src/base_system.nvgt:902:notify[0] +msg.05c95b66bd57=A resident's knife broke while butchering. +; src/bosses/bandit_hideout.nvgt:783:insert_last[0] +; src/bosses/unicorn/unicorn_boss.nvgt:697:insert_last[0] +msg.066723a92e94==== Victory Rewards === +; src/crafting/craft_materials.nvgt:483:speak_with_history[0] +msg.076a11174ca0=No space for outputs. +; src/quests/enchanted_melody_game.nvgt:36:insert_last[0] +msg.07e9ab246cdd=After closing this screen, you can practice the notes. +; src/item_registry.nvgt:item_plural:sinew +msg.08b42842d5bc=sinew +; src/bosses/unicorn/unicorn_boss.nvgt:706:insert_last[0] +msg.093440631290=You have already mastered the Rune of Swiftness. +; src/menus/altar_menu.nvgt:31:speak_with_history[0] +; src/menus/altar_menu.nvgt:59:speak_with_history[0] +msg.095b4ad6f082=Runed items cannot be sacrificed. +; excluded_sounds.nvgt:34:learn_sounds_add_description[1] +msg.096539f23463=The war drums pound! Prepare for combat! +; src/item_registry.nvgt:item_display:stones +msg.09c28ae8f83b=Stones +; src/menus/base_info.nvgt:54:insert_last[0] +msg.09d016686864=Stable not built +; src/crafting/craft_tools.nvgt:205:speak_with_history[0] +msg.0a133b0ecad2=Crafted a Fishing Pole. +; src/item_registry.nvgt:item_display:reed_baskets +msg.0a3bdcef1906=Reed Baskets +; src/quests/enchanted_melody_game.nvgt:26:insert_last[0] +msg.0a9d6438c818==== Enchanted Melody === +; src/menus/action_menu.nvgt:210:speak_with_history[0] +msg.0addf2ff4572=Burned {arg1} incense. +{arg2} hours. +; src/bosses/bandit_hideout.nvgt:305:insert_last[0] +msg.0b3d8d5cdca6=- Reach the base and destroy the barricade +; src/crafting/craft_buildings.nvgt:301:speak_with_history[0] +msg.0b78c6a3e407=Pasture must be built in the base. +; src/pet_system.nvgt:570:speak_with_history[0] +msg.0ba82f2cdb6c=Your pet is on its way. +; seed:learn_sounds_label:sounds/terrain/forest.ogg +msg.0bcd9af79f2d=forest +; seed:learn_sounds_label:sounds/crafting_complete.ogg +msg.0c7789cf9a52=crafting complete +; src/menus/action_menu.nvgt:233:insert_last[0] +msg.0c976639507d=Feed fire with stick +; src/crafting/craft_weapons.nvgt:265:speak_with_history[0] +msg.0cb01a9fb609=Crafted a Stone Axe. +; src/item_registry.nvgt:item_display:arrows +msg.0cf604cb001b=Arrows +; seed:learn_sounds_label:sounds/weapons/sling_swing.ogg +msg.0d1ec2e04286=sling swing +; src/crafting/craft_clothing.nvgt:481:speak_with_history[0] +msg.0da18cb6fbe2=Crafted a Backpack. +; src/item_registry.nvgt:item_plural:knives +msg.0e16a11cda9a=knives +; src/crafting/craft_buildings.nvgt:255:speak_with_history[0] +msg.0e2e63241760=Storage must be built in the base. +; src/item_registry.nvgt:item_singular:spear +msg.0e57fb642209=spear +; src/item_registry.nvgt:item_display:slings +msg.0eb796e65c39=Slings +; src/bosses/adventure_system.nvgt:34:insert_last[0] +msg.0f5211918a6f=Unicorn Hunt (Mountain Boss) +; src/item_registry.nvgt:item_singular:skin_pouch +msg.0f9af12cbb82=skin pouch +; src/menus/action_menu.nvgt:228:insert_last[0] +msg.10065b4cd62c=Place Snare +; src/menus/action_menu.nvgt:83:speak_with_history[0] +msg.10adaed44a3e=There is already a snare here. +; src/menus/action_menu.nvgt:99:speak_with_history[0] +msg.114e55e657be=You dump an arm load of sticks into the fire. +; src/crafting/craft_buildings.nvgt:223:speak_with_history[0] +msg.118e74882896=Herb garden can only be built in the base area. +; src/crafting/craft_barricade.nvgt:127:speak_with_history[0] +msg.125769e87318=Not enough vines. +; src/item_registry.nvgt:item_display:logs +msg.126dd3b70a5c=Logs +; src/menus/action_menu.nvgt:214:speak_with_history[0] +msg.131bd02673e5=Action menu. +; libstorm-nvgt/file_viewer.nvgt:74:screen_reader_speak[0] +msg.136e4cb03bd5=Failed to save file +; src/menus/action_menu.nvgt:170:speak_with_history[0] +msg.13cd01ceccd3=Dumped {arg1} vines into the fire. +; src/pet_system.nvgt:1252:notify[0] +msg.14056a0402ef=Your {arg1} has recovered from its injuries. +; src/menus/action_menu.nvgt:43:speak_with_history[0] +msg.14272bbf4f20=No fire nearby. +; src/item_registry.nvgt:item_plural:quivers +msg.145ba8a7a0f2=quivers +; src/bosses/bandit_hideout.nvgt:856:insert_last[0] +msg.14f5f5ff4f76=No survivors were found. +; src/crafting/craft_materials.nvgt:78:speak_with_history[0] +; src/crafting/craft_materials.nvgt:112:speak_with_history[0] +msg.15e5059dafbc=Not enough quiver capacity for arrows. +; seed:learn_sounds_label:sounds/game/game_falls.ogg +msg.16134a4250f7=game falls +; src/bosses/unicorn/unicorn_boss.nvgt:711:insert_last[0] +msg.162bb07757cf=Stable or storage not built. No horses were captured. +; src/quests/escape_from_hel_game.nvgt:12:insert_last[0] +msg.16a233f44489=- Listen for open graves approaching (growing louder) +; src/crafting/craft_barricade.nvgt:200:speak_with_history[0] +msg.16cd07c0a9b6=Reinforced barricade {arg1} times with sticks. Health now {arg2}. +; src/inventory_items.nvgt:475:speak_with_history[0] +; src/inventory_items.nvgt:498:speak_with_history[0] +; src/menus/equipment_menu.nvgt:242:speak_with_history[0] +msg.16d29946de8e={arg1} equipped. +; src/world/world_fires.nvgt:38:notify[0] +msg.17213930ddb9=Fire at x {arg1} y {arg2} is getting low! +; draugnorak.nvgt:247:ui_question[1] +; seed:manual +msg.176b9a0c8398=Really exit? +; src/item_registry.nvgt:item_singular:incense_stick +msg.17808e3775e1=incense stick +; src/crafting/craft_runes.nvgt:227:speak_with_history[0] +msg.1792abbb7eb8=Select equipment to engrave with {arg1}. {arg2} +; src/quests/skeletal_bard_game.nvgt:42:insert_last[0] +msg.17b5b2a286cc==== Skeletal Bard === +; src/quests/catch_the_boomerang_game.nvgt:30:speak_with_history[0] +msg.17c677526bd6=Press Space to throw. +; seed:learn_sounds_label:sounds/quests/bat1.ogg +msg.18b837bb6f43=bat1 +; seed:learn_sounds_label:sounds/enemies/yeti_dies.ogg +msg.18c77e824a2a=yeti dies +; src/crafting/craft_materials.nvgt:423:speak_with_history[0] +msg.1a25b0443252=Butchered boar. Got meat, 3 skins, and 2 sinew. +; seed:learn_sounds_label:sounds/enemies/bandit_dies.ogg +msg.1a853cbfbc36=bandit dies +; src/crafting/crafting_core.nvgt:206:speak_with_history[0] +msg.1af5fdafa634=Crafting... +; excluded_sounds.nvgt:48:learn_sounds_add_description[1] +msg.1bb8ef2886a3=Basic undead. +; seed:learn_sounds_label:sounds/nature/night.ogg +msg.1be2a44cb53d=night +; src/bosses/bandit_hideout.nvgt:299:insert_last[0] +msg.1bfa6bc809d4==== Bandit's Hideout === +; src/quests/escape_from_hel_game.nvgt:119:speak_with_history[0] +msg.1c2272bc0140=You fell in. Score {arg1}. +; src/crafting/craft_barricade.nvgt:163:speak_with_history[0] +msg.1c5cbc1424ec=Not enough stones. +; src/item_registry.nvgt:item_singular:skin_glove +msg.1c7022965d1a=skin glove +; src/menus/base_info.nvgt:20:insert_last[0] +msg.1d15d0d5c98d=Storage built. Total items {arg1} +; src/menus/action_menu.nvgt:160:speak_with_history[0] +msg.1d493d7011a0=No vines to feed fire. +; src/bosses/bandit_hideout.nvgt:802:insert_last[0] +msg.1d61343b9660=Storage not built. No item rewards. +; seed:learn_sounds_label:sounds/items/fire.ogg +msg.1da840244989=fire +; src/item_registry.nvgt:item_singular:sling +msg.1db76ac85135=sling +; src/pet_system.nvgt:484:speak_with_history[0] +msg.20a5cf945886=Your {arg1} is unconscious. You can't abandon it right now. +; src/world/world_snares.nvgt:81:notify[0] +msg.21192381c66d=A {arg1} escaped from your snare at x {arg2} y 0! +; src/quest_system.nvgt:178:text_reader[1] +msg.21b534113694=Quest Rewards +; src/menus/action_menu.nvgt:260:insert_last[0] +msg.226f7d1cdeb1=Check fishing pole +; src/item_registry.nvgt:item_singular:axe +msg.237b5017397b=axe +; src/time_system.nvgt:658:notify[0] +msg.244b7f948d5d=The {arg1} invasion has ended. +; src/bosses/adventure_system.nvgt:21:speak_with_history[0] +msg.2455422b697c=You have already attempted an adventure today. +; src/bosses/unicorn/unicorn_boss.nvgt:117:insert_last[0] +msg.245c89590526=- When the bridge collapses with the Unicorn on it, you win! +; draugnorak.nvgt:296:speak_with_history[0] +; src/menus/menu_utils.nvgt:53:speak_with_history[0] +msg.247e80c6393a=You have died. +; src/environment.nvgt:980:speak_with_history[0] +msg.24d4bced9a39=Climbing down. +; draugnorak.nvgt:425:speak_with_history[0] +; src/bosses/bandit_hideout.nvgt:400:speak_with_history[0] +; src/bosses/unicorn/unicorn_boss.nvgt:231:speak_with_history[0] +msg.25038d9da464=east +; src/bosses/bandit_hideout.nvgt:780:speak_with_history[0] +; src/bosses/unicorn/unicorn_boss.nvgt:682:speak_with_history[0] +msg.25296a531b53=Victory! +; src/bosses/bandit_hideout.nvgt:830:insert_last[0] +msg.25db7a4ec692=Livestock were recovered and added to your pasture. +; seed:learn_sounds_label:sounds/quests/safe_to_jump.ogg +msg.2630bd23d7d4=safe to jump +; seed:learn_sounds_label:sounds/quests/spear_hit.ogg +; seed:learn_sounds_label:sounds/weapons/spear_hit.ogg +msg.2648db667f58=spear hit +; src/save_system.nvgt:439:speak_with_history[0] +msg.26c2f4289162=Load game. Select character. Press delete to remove a save. +; seed:learn_sounds_label:sounds/nature/thunder_medium.ogg +msg.270dcc07d3ae=thunder medium +; seed:learn_sounds_label:sounds/jump.ogg +; seed:learn_sounds_label:sounds/quests/jump.ogg +msg.271e9a568c0e=jump +; src/menus/base_info.nvgt:46:insert_last[0] +msg.2727056eb878=Pasture built. Livestock {arg1} of {arg2} +; libstorm-nvgt/notifications.nvgt:141:notifications_speak[0] +; libstorm-nvgt/notifications.nvgt:161:notifications_speak[0] +; libstorm-nvgt/notifications.nvgt:181:notifications_speak[0] +msg.273d131f0c8e=No notifications. +; src/time_system.nvgt:348:notify[0] +msg.27a582f91d6b={arg1} are invading from {arg2}! +; src/quests/enchanted_melody_game.nvgt:28:insert_last[0] +msg.27bbfb11a294=Repeat the magical pattern to earn favor from the gods. +; seed:learn_sounds_label:sounds/enemies/vampyr4.ogg +msg.27e4cdae51e3=vampyr4 +; src/quests/skeletal_bard_game.nvgt:50:insert_last[0] +msg.27f1dd42841b=- Press Enter to confirm +; src/menus/base_info.nvgt:30:insert_last[0] +msg.27f5254de319=Storage not built +; src/bosses/bandit_hideout.nvgt:813:insert_last[0] +msg.27f76f96867d={arg1}: +{arg2}. +; src/bosses/unicorn/unicorn_boss.nvgt:115:insert_last[0] +msg.282b619d84bc=- Or fight the Unicorn directly (it has massive health) +; src/quests/bat_invasion_game.nvgt:13:insert_last[0] +msg.285df50e183e=- Press SPACE to throw your spear when the bat sounds centered +; src/menus/action_menu.nvgt:57:speak_with_history[0] +msg.29a2d9e8aadb=Snare holds a {arg1}. +; src/crafting/craft_clothing.nvgt:176:speak_with_history[0] +msg.29a828259ef4=Crafted {arg1} Skin Hats. +; src/crafting/craft_tools.nvgt:453:speak_with_history[0] +msg.29d1fab76b47=Crafted a reed basket. +; src/quests/bat_invasion_game.nvgt:19:text_reader_lines[1] +; src/quests/catch_the_boomerang_game.nvgt:16:text_reader_lines[1] +; src/quests/enchanted_melody_game.nvgt:39:text_reader_lines[1] +msg.29f291640516=Quest Instructions +; src/bosses/unicorn/unicorn_boss.nvgt:720:insert_last[0] +msg.2a72e1170f55=No horses were captured. +; src/quests/escape_from_hel_game.nvgt:11:insert_last[0] +msg.2afb87ae1752=- You run automatically, speed increases over time +; src/flying_creature_template.nvgt:70:speak_with_history[0] +msg.2b436c9ddbbb=Butchered duck. Got 1 meat, feathers, and down. +; src/menus/altar_menu.nvgt:16:speak_with_history[0] +msg.2b8ff62c083e=Must be in base to use altar. +; src/fylgja_system.nvgt:101:insert_last[0] +msg.2badd283a63b=You have already unlocked the {arg1} Fylgja. +; excluded_sounds.nvgt:29:learn_sounds_add_description[1] +msg.2bb11d7c39dc=An enemy is in range of your currently wielded weapon. +; seed:learn_sounds_label:sounds/bosses/unicorn/unicorn_falls.ogg +msg.2bf7b1d59f34=unicorn falls +; src/item_registry.nvgt:item_display:skin_hats +msg.2c6c80931cd1=Skin Hats +; src/crafting/craft_materials.nvgt:279:speak_with_history[0] +msg.2cd0d08a829a=Crafted {arg1} Incense. +; draugnorak.nvgt:183:speak_with_history[0] +msg.2db4abf99659=New game started. +; seed:learn_sounds_label:sounds/weapons/spear_swing.ogg +msg.2e094dd0a2ab=spear swing +; src/quests/catch_the_boomerang_game.nvgt:90:speak_with_history[0] +msg.2efa88864653=The boomerang hit you.{arg1} +; src/item_registry.nvgt:item_display:fish +msg.3030e8ec7633=Fish +; src/crafting/craft_materials.nvgt:368:speak_with_history[0] +msg.30680e90d434=Smoked {arg1} fish into {arg2} smoked fish. +; src/crafting/craft_materials.nvgt:71:speak_with_history[0] +; src/crafting/craft_materials.nvgt:104:speak_with_history[0] +msg.311f421748f9=You need a quiver to craft arrows. +; src/pet_system.nvgt:466:speak_with_history[0] +msg.31342e72b48c={arg1} leaves. +; src/crafting/craft_buildings.nvgt:305:speak_with_history[0] +msg.32b43edbf3b0=Storage must be upgraded before a pasture. +; src/crafting/craft_tools.nvgt:306:speak_with_history[0] +msg.32c8e74e0055=Crafted a Quiver. +; src/quests/skeletal_bard_game.nvgt:48:insert_last[0] +msg.33bc40d8201e=- After the tune, choose the number of notes +; src/crafting/craft_materials.nvgt:168:speak_with_history[0] +msg.33c75a645734=Crafted a bowstring. +; src/item_registry.nvgt:item_singular:quiver +msg.33c89b4bdc4e=quiver +; seed:learn_sounds_label:sounds/menu/menu_move.ogg +; seed:learn_sounds_label:sounds/menu.bak/menu_move.ogg +msg.34daac1528e0=menu move +; src/crafting/craft_buildings.nvgt:335:speak_with_history[0] +msg.34e4ec0621b4=Storage must be upgraded before a stable. +; src/item_registry.nvgt:item_singular:fishing_pole +msg.34fcedc354d9=fishing pole +; src/menus/base_info.nvgt:26:insert_last[0] +msg.363b1d2abd57=Food in storage {arg1} meat, {arg2} smoked fish, {arg3} baskets of fruits and nuts. Total {arg4}. Daily use {arg5} +; src/bosses/unicorn/unicorn_boss.nvgt:728:text_reader_lines[1] +msg.364118a099cd=Unicorn Victory +; src/fylgja_system.nvgt:91:insert_last[0] +msg.36e876dbcc17=You have a {arg1} connection with the {arg2}. +; src/item_registry.nvgt:item_plural:skins +msg.37423f3bd00f=skins +; src/menus/action_menu.nvgt:61:speak_with_history[0] +msg.37bb70fed953=Snare is set and empty. +; src/item_registry.nvgt:item_singular:vine +; seed:learn_sounds_label:sounds/items/vine.ogg +msg.37ef0562902c=vine +; src/item_registry.nvgt:item_display:incense +msg.381aea2617cb=Incense +; seed:learn_sounds_label:sounds/nature/thunder_low.ogg +msg.38a7045d078c=thunder low +; seed:learn_sounds_label:sounds/weapons/arrow_flies.ogg +msg.38eb0fe3f78a=arrow flies +; src/menus/action_menu.nvgt:59:speak_with_history[0] +msg.39268e59f7ba=Snare is set but not active yet. +; src/pet_system.nvgt:551:speak_with_history[0] +msg.3976f81bbfef=Your {arg1} is leaving. +; src/item_registry.nvgt:item_singular:piece_of_sinew +msg.39784ab5ac68=piece of sinew +; src/item_registry.nvgt:item_singular:arrow +msg.39a531d92bbe=arrow +; seed:learn_sounds_label:sounds/items/item.ogg +msg.3a7d9767b123=item +; src/crafting/craft_runes.nvgt:168:speak_with_history[0] +msg.3ba80d6f0cfb=Runes. {arg1} +; src/environment.nvgt:930:speak_with_history[0] +msg.3bc5191d2dc2=Started climbing tree. Height is {arg1} feet. +; src/item_registry.nvgt:item_plural:skin_pouches +msg.3c32451c9c71=skin pouches +; src/bosses/unicorn/unicorn_boss.nvgt:713:insert_last[0] +msg.3ce35dadcc67=Stable is full. No horses were captured. +; seed:learn_sounds_label:sounds/actions/fishpole.ogg +msg.3cebb4c50866=fishpole +; src/crafting/craft_clothing.nvgt:233:speak_with_history[0] +msg.3dd6d1aafd1c=Crafted {arg1} Skin Gloves. +; seed:learn_sounds_label:sounds/terrain/hard_stone.ogg +msg.3e024b87e650=hard stone +; src/item_registry.nvgt:item_display:skins +msg.3e03229d39e4=Skins +; src/time_system.nvgt:836:notify[0] +msg.3e2cee02d8c1=The sky begins to brighten. +; src/crafting/craft_buildings.nvgt:100:speak_with_history[0] +msg.3e575fbcccad=No buildings available. +; src/world/world_fires.nvgt:44:notify[0] +msg.3e919b422247=Fire at x {arg1} y {arg2} has gone out. +; src/item_registry.nvgt:item_plural:skin_gloves +msg.3eb9cd52ec8d=skin gloves +; draugnorak.nvgt:229:speak_with_history[0] +; src/bosses/bandit_hideout.nvgt:322:speak_with_history[0] +; src/bosses/unicorn/unicorn_boss.nvgt:135:speak_with_history[0] +msg.3f098cc2c738=Paused. Press backspace to resume. +; src/crafting/craft_clothing.nvgt:345:speak_with_history[0] +msg.3f35ee808cb2=Crafted {arg1} Skin Tunics. +; src/quests/enchanted_melody_game.nvgt:34:insert_last[0] +msg.3f638a51cb47=E or U - Fourth note (highest pitch) +; src/environment.nvgt:852:speak_with_history[0] +msg.3fab86f11579=This tree is empty. +; src/base_system.nvgt:780:notify[0] +msg.3ffc8c72cadf=A {arg1} escaped while a resident checked the snare at x {arg2}. +; seed:learn_sounds_label:sounds/nature/thunder_high.ogg +msg.4010ceb056dd=thunder high +; seed:learn_sounds_label:sounds/quests/bone4.ogg +msg.403654dac805=bone4 +; src/item_registry.nvgt:item_plural:spears +msg.4037ea7ad431=spears +; seed:learn_sounds_label:sounds/enemies/wight1.ogg +msg.408a612a4167=wight1 +; src/crafting/craft_buildings.nvgt:364:speak_with_history[0] +msg.409251fa0d30=Altar must be built in the base. +; src/item_registry.nvgt:item_singular:reed_basket +msg.412fed81f58a=reed basket +; seed:learn_sounds_label:sounds/nature/wind_medium.ogg +msg.417705583821=wind medium +; seed:learn_sounds_label:sounds/terrain/fly.ogg +msg.41a682cadfe3=fly +; src/menus/action_menu.nvgt:67:speak_with_history[0] +msg.423b7f343fb8=Fish on the line. +; src/menus/action_menu.nvgt:140:speak_with_history[0] +msg.4248c4c1f5a9=Incense burning. {arg1} hours remaining. +; src/crafting/craft_weapons.nvgt:184:speak_with_history[0] +msg.425dbf90058e=Crafted {arg1} Slings. +; src/item_registry.nvgt:item_singular:boar_carcass +msg.4272004d80b4=boar carcass +; src/item_registry.nvgt:item_display:skin_pouches +msg.427944aa680d=Skin Pouches +; src/quests/bat_invasion_game.nvgt:14:insert_last[0] +msg.42b2620d1afa=- Accurate throws earn 2 points each +; src/quests/escape_from_hel_game.nvgt:14:insert_last[0] +msg.42da7d0b84a8=- Each successful jump earns 2 points +; libstorm-nvgt/file_viewer.nvgt:53:screen_reader_speak[0] +msg.434d81f4ecc4=Failed to open file: {arg1} +; src/crafting/craft_runes.nvgt:303:speak_with_history[0] +msg.43696d3d5fb8=Engraved {arg1}. +; seed:learn_sounds_label:sounds/quests/spear_miss.ogg +msg.437ecef821db=spear miss +; src/item_registry.nvgt:item_plural:bowstrings +msg.43821cae64c5=bowstrings +; src/menus/action_menu.nvgt:155:speak_with_history[0] +msg.44328bf27ea3=Dumped {arg1} sticks into the fire. +; seed:learn_sounds_label:sounds/actions/falling.ogg +msg.444d4678962b=falling +; src/crafting/craft_runes.nvgt:222:speak_with_history[0] +msg.4525b0152629=No equipment available to engrave. +; seed:learn_sounds_label:sounds/weapons/bow_fire.ogg +msg.4537c16badd9=bow fire +; src/item_registry.nvgt:item_plural:snares +msg.455b11a16eb7=snares +; src/bosses/bandit_hideout.nvgt:384:speak_with_history[0] +msg.45a604519c0d=The bandits cut you down. +; src/fishing.nvgt:168:speak_with_history[0] +msg.45bcf18418f1=You need to be within 2 tiles of a stream. +; seed:learn_sounds_label:sounds/terrain/draugrhaugr_step.ogg +msg.46fcdd91c99e=draugrhaugr step +; src/crafting/craft_tools.nvgt:287:speak_with_history[0] +msg.470f9d20c2b3=Crafted {arg1} Rope. +; src/item_registry.nvgt:item_plural:logs +msg.474c797713f3=logs +; src/quests/skeletal_bard_game.nvgt:100:speak_with_history[0] +msg.4798100ccc16=Skeletal Bard complete. Your score for all five tunes is {arg1}. +; src/item_registry.nvgt:item_display:snares +msg.483a17dd2a17=Snares +; src/crafting/craft_weapons.nvgt:241:speak_with_history[0] +msg.484adfaab5cd=Crafted {arg1} Bows. +; seed:learn_sounds_label:sounds/enemies/draugrhaugr1.ogg +msg.494f87e3874d=draugrhaugr1 +; src/item_registry.nvgt:item_plural:axes +msg.495f723e9fa1=axes +; seed:learn_sounds_label:sounds/terrain/gravel.ogg +msg.4a0e932d1bc5=gravel +; src/item_registry.nvgt:item_plural:skin_tunics +msg.4a17d4fabe93=skin tunics +; seed:learn_sounds_label:sounds/quests/boomerang.ogg +msg.4b9b4385b1a9=boomerang +; src/menus/base_info.nvgt:6:speak_with_history[0] +; src/quest_system.nvgt:98:speak_with_history[0] +msg.4bce12c3add1=You are not in the base. +; src/menus/storage_menu.nvgt:171:speak_with_history[0] +msg.4bd5f5ba3c71=Withdrew {arg1}. +; src/item_registry.nvgt:item_plural:stones +msg.4c0d2469f78b=stones +; src/crafting/craft_buildings.nvgt:158:speak_with_history[0] +msg.4c8578f10e74=There is already a firepit in the base. +; src/menus/action_menu.nvgt:117:speak_with_history[0] +msg.4d723065565f=You heave a log into the fire. +; src/crafting/craft_tools.nvgt:539:speak_with_history[0] +msg.4db797458510=Crafted {arg1} Clay Pots. +; src/pet_system.nvgt:555:speak_with_history[0] +msg.4e5ee9d0128e=No pet. +; src/crafting/craft_runes.nvgt:146:speak_with_history[0] +msg.4ea30f60b92f=Rune engraving can only be done in the base area. +; src/crafting/craft_buildings.nvgt:166:speak_with_history[0] +msg.4ecf016b2d3f=There is already a firepit here. +; seed:learn_sounds_label:sounds/notify.ogg +msg.4f08301dcb7b=notify +; seed:learn_sounds_label:sounds/terrain/shallow_water.ogg +msg.4f11c6797b8d=shallow water +; src/crafting/craft_buildings.nvgt:356:speak_with_history[0] +msg.4f4dcdd4262d=Stable built. +; src/item_registry.nvgt:item_display:bows +msg.4f773e9d5bf0=Bows +; seed:learn_sounds_label:sounds/game/goose.ogg +msg.4f7f358cd341=goose +; src/item_registry.nvgt:item_display:clay_pots +msg.4f99d3d5b638=Clay Pots +; src/quests/catch_the_boomerang_game.nvgt:75:speak_with_history[0] +msg.4ff23d0c3700=You missed the boomerang.{arg1} +; seed:learn_sounds_label:sounds/game/turkey.ogg +msg.5009c8e190ee=turkey +; src/quests/skeletal_bard_game.nvgt:49:insert_last[0] +msg.508e8bc0cc57=- Use Up/Down to change the number +; seed:learn_sounds_label:sounds/weapons/sling_low_range.ogg +msg.5123c22ce6a5=sling low range +; src/environment.nvgt:966:speak_with_history[0] +msg.514b1402c9f3=Safely reached the ground. +; src/environment.nvgt:954:speak_with_history[0] +msg.515f45b3270f=Reached the top at {arg1} feet. +; src/bosses/unicorn/unicorn_boss.nvgt:699:insert_last[0] +msg.52140591224d=The gods are pleased with your victory! {arg1} favor awarded. +; draugnorak.nvgt:79:insert_last[0] +msg.528fb1c4e5fb=New Game +; src/crafting/craft_runes.nvgt:284:speak_with_history[0] +; src/crafting/craft_runes.nvgt:317:speak_with_history[0] +msg.529f0f749181=No {arg1} available. +; src/menus/action_menu.nvgt:247:insert_last[0] +msg.52f1fd1916c9=Burn incense +; src/pet_system.nvgt:901:insert_last[0] +msg.5397e0583f14=Yes +; src/environment.nvgt:768:speak_with_history[0] +msg.53da80cba4ff=Collected {arg1} and snare. +; seed:learn_sounds_label:sounds/quests/squish.ogg +msg.5460f2e3c43e=squish +; seed:learn_sounds_label:sounds/actions/fishpole_break.ogg +msg.546330ee64db=fishpole break +; src/menus/action_menu.nvgt:89:speak_with_history[0] +msg.546f2ff04577=Snare set. +; draugnorak.nvgt:187:ui_info_box[0] +; draugnorak.nvgt:201:ui_info_box[0] +; draugnorak.nvgt:587:ui_info_box[0] +msg.54fc2d657ed6=Draugnorak +; seed:learn_sounds_label:sounds/enemies/vampyr3.ogg +msg.55748cf43121=vampyr3 +; src/crafting/craft_clothing.nvgt:308:speak_with_history[0] +msg.55c9d2b3e8a4=Crafted a Skin Tunic. +; src/quests/bat_invasion_game.nvgt:9:insert_last[0] +msg.5665ef30ef4d=Bats are invading! Throw your spear to defend. +; src/time_system.nvgt:337:notify[0] +msg.57b0465e575a=A mountain range has been discovered to the east! +; src/menus/action_menu.nvgt:53:speak_with_history[0] +msg.57e2db156a64=No snare nearby. +; src/menus/action_menu.nvgt:241:insert_last[0] +msg.57e457c02ba4=Feed fire with log +; src/item_registry.nvgt:item_display:reeds +msg.584227d57290=Reeds +; seed:learn_sounds_label:sounds/enemies/vampyr2.ogg +msg.5906872f9bb6=vampyr2 +; seed:manual +msg.596350ae8387=Found a reed. +; src/item_registry.nvgt:item_display:boar_carcasses +msg.599096c44c5b=Boar Carcasses +; libstorm-nvgt/learn_sounds.nvgt:361:learn_sounds_speak[0] +msg.5993f9da16fc=Learn sounds. +; src/quests/bat_invasion_game.nvgt:104:speak_with_history[0] +msg.59b74cafdedb=Bat invasion complete. Score {arg1}. +; src/pet_system.nvgt:1267:notify[0] +msg.59eab77f00cc=A {arg1} is getting hungry. +; src/crafting/craft_tools.nvgt:186:speak_with_history[0] +msg.5a7c581c0427=Crafted {arg1} Snares. +; seed:learn_sounds_label:sounds/enemies/wight_dies.ogg +msg.5b61e4982b59=wight dies +; src/item_registry.nvgt:item_display:feathers +msg.5bdb7fa2223d=Feathers +; src/time_system.nvgt:716:speak_with_history[0] +msg.5be2899acbe8=The residents' purpose fades. +; src/item_registry.nvgt:item_plural:reeds +msg.5c2434ecd90f=reeds +; src/bosses/bandit_hideout.nvgt:825:insert_last[0] +msg.5cac31311668=Pasture is full. No livestock were recovered. +; src/bosses/unicorn/unicorn_boss.nvgt:110:insert_last[0] +msg.5cc7429297c8=that can be destroyed with an axe. +; src/pet_system.nvgt:575:speak_with_history[0] +msg.5cda1b25dd32=A {arg1} is hungry and unresponsive. +; src/enemies/undead.nvgt:387:speak_with_history[0] +msg.5d1fa9e5a5ef=A resident has been taken. +; src/time_system.nvgt:282:notify[0] +msg.5de6d49b4e89=The area has expanded! New territory discovered to the east. +; seed:learn_sounds_label:sounds/actions/cast_strength.ogg +msg.5dfc869b7588=cast strength +; src/menus/action_menu.nvgt:91:speak_with_history[0] +msg.5e09b31082c5=No snares to place. +; src/time_system.nvgt:779:notify[0] +msg.5ea84b1201bb={arg1} favor grants you the eyes of an eagle. +; seed:learn_sounds_label:sounds/menu/menu_select.ogg +; seed:learn_sounds_label:sounds/menu.bak/menu_select.ogg +msg.5f3f3a85393b=menu select +; src/bosses/adventure_system.nvgt:16:speak_with_history[0] +msg.600007c39e3f=No adventures available in the base. +; src/crafting/craft_barricade.nvgt:11:speak_with_history[0] +; src/crafting/craft_barricade.nvgt:105:speak_with_history[0] +; src/crafting/craft_barricade.nvgt:123:speak_with_history[0] +msg.612e052d92d2=Barricade is already at full health. +; src/menus/action_menu.nvgt:48:speak_with_history[0] +msg.61b1ce299103={arg1}. {arg2} remaining. +; src/menus/base_info.nvgt:12:insert_last[0] +msg.623f78278373=Barricade health {arg1} of {arg2} +; src/bosses/bandit_hideout.nvgt:823:insert_last[0] +msg.62583aa87a59=Pasture or storage not built. No livestock were recovered. +; seed:learn_sounds_label:sounds/actions/climb_tree.ogg +msg.627a5b9e6322=climb tree +; src/crafting/craft_weapons.nvgt:79:speak_with_history[0] +msg.62b1e8703ede=Crafted a Spear. +; seed:learn_sounds_label:sounds/pets/hawk.ogg +msg.62fac2f438bb=hawk +; src/item_registry.nvgt:item_plural:fish +; src/item_registry.nvgt:item_singular:fish +msg.64875fcccaac=fish +; src/menus/base_info.nvgt:43:insert_last[0] +msg.64b945d9072e=Herb garden not built +; src/quests/catch_the_boomerang_game.nvgt:100:speak_with_history[0] +msg.656dac78b857=You caught {arg1} boomerangs for a total of {arg2} points. +; src/item_registry.nvgt:item_plural:skin_pants +; src/item_registry.nvgt:item_singular:skin_pants +msg.65eb0fb6ab40=skin pants +; src/quest_system.nvgt:102:speak_with_history[0] +msg.666d7125f17b=No quests available. +; src/item_registry.nvgt:item_plural:vines +msg.66fd9a7e91f8=vines +; src/menus/base_info.nvgt:41:insert_last[0] +msg.67ae077e6458=Herb garden built +; seed:learn_sounds_label:sounds/quests/bone3.ogg +msg.686021c25738=bone3 +; src/quests/catch_the_boomerang_game.nvgt:11:insert_last[0] +msg.6861fd95f4f7=- There are 5 turns +; src/menus/storage_menu.nvgt:67:speak_with_history[0] +msg.6a26c7cfd66b=Deposited {arg1}. +; src/quests/catch_the_boomerang_game.nvgt:71:speak_with_history[0] +msg.6a33218f6325=You {arg1} the boomerang. {arg2} points.{arg3} +; seed:learn_sounds_label:sounds/weapons/axe_swing.ogg +msg.6c265b98505c=axe swing +; libstorm-nvgt/learn_sounds.nvgt:405:learn_sounds_speak[0] +msg.6cbfe49b4adf=Unable to load sound. +; src/item_registry.nvgt:item_display:canoes +msg.6cf4935744c0=Canoes +; seed:learn_sounds_label:sounds/bosses/bandit/base_destroyed.ogg +msg.6d18e60e7952=base destroyed +; libstorm-nvgt/file_viewer.nvgt:70:screen_reader_speak[0] +msg.6d4a0243dbfd=File saved successfully +; src/item_registry.nvgt:item_plural:clay_pots +msg.6d9c2a31fbbc=clay pots +; src/quests/enchanted_melody_game.nvgt:37:insert_last[0] +msg.6e247a17f21a=Press Enter when ready to begin, or Escape to cancel. +; src/item_registry.nvgt:item_singular:knife +msg.6e32618ee300=knife +; src/bosses/unicorn/unicorn_boss.nvgt:704:insert_last[0] +msg.6fdc01a81c89=You can now engrave equipment with this rune at the crafting menu. +; seed:learn_sounds_label:sounds/actions/hit_ground.ogg +msg.704e634dd69f=hit ground +; src/crafting/craft_tools.nvgt:344:speak_with_history[0] +msg.708019666e3c=Crafted {arg1} Quivers. +; src/menus/action_menu.nvgt:124:speak_with_history[0] +; src/menus/action_menu.nvgt:190:speak_with_history[0] +; src/menus/altar_menu.nvgt:22:speak_with_history[0] +msg.70a5f57b592d=No altar built. +; seed:learn_sounds_label:sounds/actions/set_snare.ogg +msg.714165bec103=set snare +; src/quests/skeletal_bard_game.nvgt:51:insert_last[0] +msg.717d7fcc68b7=- There are 5 rounds, up to 4 points each +; src/item_registry.nvgt:item_plural:skin_hats +msg.71aad5f523eb=skin hats +; libstorm-nvgt/learn_sounds.nvgt:375:learn_sounds_speak[0] +; src/bosses/adventure_system.nvgt:60:speak_with_history[0] +; src/crafting/craft_barricade.nvgt:53:speak_with_history[0] +msg.731152a028c6=Closed. +; src/base_system.nvgt:1084:speak_with_history[0] +msg.757117781964=Residents gathered {arg1} baskets of fruits and nuts. +; src/quests/catch_the_boomerang_game.nvgt:10:insert_last[0] +msg.7680f62917b1=- Press Space again when it sounds close +; src/menus/action_menu.nvgt:132:speak_with_history[0] +; src/menus/action_menu.nvgt:198:speak_with_history[0] +msg.76c1490aad8c=No incense to burn. +; src/item_registry.nvgt:item_plural:down +; src/item_registry.nvgt:item_singular:down +msg.77346d0447da=down +; seed:learn_sounds_label:sounds/terrain/deep_forest.ogg +msg.7748b68d141a=deep forest +; src/crafting/craft_materials.nvgt:418:speak_with_history[0] +msg.778a4ff05e50=Butchered turkey. Got 1 meat and feathers. +; src/crafting/craft_buildings.nvgt:368:speak_with_history[0] +msg.7822e0af08c8=Altar already built. +; src/bosses/unicorn/unicorn_boss.nvgt:107:insert_last[0] +msg.78f036ea9ebd=The beast charges back and forth relentlessly. +; seed:learn_sounds_label:sounds/crafting.ogg +msg.7921110eb7e1=crafting +; src/item_registry.nvgt:item_plural:fishing_poles +msg.7968afd0c044=fishing poles +; src/menus/action_menu.nvgt:128:speak_with_history[0] +; src/menus/action_menu.nvgt:194:speak_with_history[0] +msg.7a61d6b17282=You need a clay pot to burn incense. +; src/crafting/craft_tools.nvgt:481:speak_with_history[0] +msg.7b50bfc0ef4b=Crafted {arg1} Reed Baskets. +; src/item_registry.nvgt:item_singular:log +msg.7babc233de26=log +; src/item_registry.nvgt:item_plural:arrows +msg.7c49f713a0fb=arrows +; seed:manual +msg.7c57f16628c7=Found clay. +; src/environment.nvgt:1136:speak_with_history[0] +msg.7cc940c217a7=Climbing {arg1}. {arg2} feet. +; src/quests/skeletal_bard_game.nvgt:45:insert_last[0] +msg.7d2cf35450e3=Listen to the melody and count the notes. +; seed:learn_sounds_label:sounds/enemies/zombie1.ogg +msg.7d88f361f3e8=zombie1 +; seed:learn_sounds_label:sounds/enemies/bandit_female_dies.ogg +msg.7e83de91b4a2=bandit female dies +; src/crafting/craft_weapons.nvgt:312:speak_with_history[0] +msg.7f6990bc78a8=Crafted {arg1} Stone Axes. +; excluded_sounds.nvgt:31:learn_sounds_add_description[1] +msg.7f7e3bbf8864=An enemy is no longer in range of your currently wielded weapon. +; src/item_registry.nvgt:item_display:backpacks +msg.7fae743693f4=Backpacks +; draugnorak.nvgt:187:ui_info_box[1] +; draugnorak.nvgt:201:ui_info_box[1] +msg.802aa6576577=Load Game +; src/item_registry.nvgt:item_plural:sticks +msg.803e4c41bbd2=sticks +; seed:learn_sounds_label:sounds/environment/tree.ogg +; seed:learn_sounds_label:sounds/items/tree.ogg +msg.80655da8d80a=tree +; src/item_registry.nvgt:item_plural:canoes +msg.806baaf8e039=canoes +; src/pet_system.nvgt:902:insert_last[0] +msg.816c52fd2bdd=No +; src/time_system.nvgt:761:notify[0] +; src/time_system.nvgt:771:notify[0] +msg.81df04a81e24={arg1} favor shines upon you. {arg2} +; src/quests/catch_the_boomerang_game.nvgt:12:insert_last[0] +msg.823be948275f=- Better timing earns more points (up to 4) +; src/menus/action_menu.nvgt:185:speak_with_history[0] +msg.8257ec29507d=Dumped {arg1} logs into the fire. +; src/crafting/craft_barricade.nvgt:286:speak_with_history[0] +msg.837affd1b6ba=Reinforced barricade {arg1} times with stones. Health now {arg2}. +; excluded_sounds.nvgt:41:learn_sounds_add_description[1] +msg.83865f248682=More powerful undead. They do not lose track of you when you are in the base. +; src/crafting/craft_tools.nvgt:102:speak_with_history[0] +msg.83a5c4369598=Crafted a Stone Knife. +; src/menus/equipment_menu.nvgt:221:speak_with_history[0] +msg.83fa126f6e3f={arg1} set to slot {arg2}. +; src/fylgja_system.nvgt:137:speak_with_history[0] +msg.8420d3438dd8=Fylgja menu. +; seed:learn_sounds_label:sounds/nature/wind_high.ogg +msg.8443de859917=wind high +; src/quests/escape_from_hel_game.nvgt:7:insert_last[0] +msg.849a4f4a399d=You are fleeing from the realm of the dead! +; src/crafting/craft_tools.nvgt:129:speak_with_history[0] +msg.84c5ff34e186=Crafted {arg1} Stone Knives. +; src/time_system.nvgt:932:notify[0] +msg.8551c25cde92=The incense has burned out. +; src/bosses/bandit_hideout.nvgt:359:speak_with_history[0] +msg.8569a552d825=x {arg1}, terrain {arg2}. Base {arg3} tiles east. Barricade {arg4} of {arg5}. +; seed:learn_sounds_label:sounds/quests/bone2.ogg +msg.85a177a79f1d=bone2 +; src/item_registry.nvgt:item_singular:feather +msg.85f15bc57d6c=feather +; src/item_registry.nvgt:item_singular:canoe +msg.862021235188=canoe +; src/base_system.nvgt:147:notify[0] +msg.862614facb79=No food, residents are hungry. +; src/crafting/craft_clothing.nvgt:195:speak_with_history[0] +msg.8626f8c56627=Crafted Skin Gloves. +; seed:learn_sounds_label:sounds/quests/bone5.ogg +msg.86290fe7e711=bone5 +; libstorm-nvgt/notifications.nvgt:148:notifications_speak[0] +msg.86e257c3efcb=Oldest notification. {arg1} +; src/item_registry.nvgt:item_display:bowstrings +msg.87383ce4344d=Bowstrings +; src/item_registry.nvgt:item_plural:ropes +msg.87cdb5c91437=ropes +; src/crafting/craft_buildings.nvgt:382:speak_with_history[0] +msg.87ee697acb01=Altar built. +; src/time_system.nvgt:775:notify[0] +msg.8818f1d55166={arg1} radiance fills residents with purpose. +; src/bosses/bandit_hideout.nvgt:340:speak_with_history[0] +msg.885cda3d707c=You flee the hideout. +; src/quests/bat_invasion_game.nvgt:7:insert_last[0] +msg.88749dbbbf09==== Bat Invasion === +; src/bosses/unicorn/unicorn_boss.nvgt:114:insert_last[0] +msg.88a8bee5206e=- Lure the Unicorn onto the bridge +; src/menus/action_menu.nvgt:256:insert_last[0] +msg.8aab96177c7d=Check snare +; src/environment.nvgt:907:speak_with_history[0] +msg.8b9881d57a1a=This area has nothing left. +; src/quests/enchanted_melody_game.nvgt:99:speak_with_history[0] +msg.8c3fa20e31d0=You matched {arg1} notes. Score {arg2}. +; src/save_system.nvgt:476:speak_with_history[0] +msg.8d0ad3cf8648=Save deleted. +; src/fishing.nvgt:354:speak_with_history[0] +; src/fishing.nvgt:382:speak_with_history[0] +msg.8d6c04ec96c7=The fish is still on the line. +; src/inventory_items.nvgt:481:speak_with_history[0] +; src/menus/equipment_menu.nvgt:237:speak_with_history[0] +msg.8e19643ce0e6={arg1} unequipped. +; src/item_registry.nvgt:item_plural:moccasins +msg.8ee83610f314=moccasins +; src/item_registry.nvgt:item_display:skin_pants +msg.8f1379e43b0f=Skin Pants +; src/quests/catch_the_boomerang_game.nvgt:6:insert_last[0] +msg.8f69276b9332=Throw a boomerang and catch it on the return. +; src/bosses/unicorn/unicorn_boss.nvgt:106:insert_last[0] +msg.8f993ac20023=You enter a narrow mountain pass. A massive Unicorn blocks your path! +; src/menus/base_info.nvgt:64:speak_with_history[0] +msg.8feaa2e73a19=Base info. {arg1} +; src/crafting/craft_barricade.nvgt:109:speak_with_history[0] +msg.9007bd6e52db=Not enough sticks. +; src/menus/altar_menu.nvgt:87:speak_with_history[0] +msg.90c991ce4eec=Altar. Favor {arg1}. +; seed:learn_sounds_label:sounds/quests/bee.ogg +msg.917ee46db0cd=bee +; src/crafting/craft_barricade.nvgt:229:speak_with_history[0] +msg.922b0aa0ef1a=Reinforced barricade {arg1} times with vines. Health now {arg2}. +; seed:learn_sounds_label:sounds/player_male_damage.ogg +msg.924e0eb6b6f1=player male damage +; src/time_system.nvgt:766:notify[0] +msg.9281d2fe5f08={arg1} favor shines upon you. You feel swift for a while. +; src/bosses/unicorn/unicorn_boss.nvgt:718:insert_last[0] +msg.92849349ef1f=A horse joins your stable. +; seed:learn_sounds_label:sounds/actions/climb_rope.ogg +msg.92abc6d6953f=climb rope +; src/fylgja_system.nvgt:99:insert_last[0] +msg.92b702e15649=You have unlocked the {arg1} Fylgja! +; src/crafting/craft_materials.nvgt:230:speak_with_history[0] +msg.93682970720f=Crafted incense. +; src/bosses/bandit_hideout.nvgt:796:insert_last[0] +msg.9379874d592b=Altar not built. No favor awarded. +; src/crafting/craft_clothing.nvgt:402:speak_with_history[0] +msg.93852d36202b=Crafted {arg1} Moccasins. +; src/bosses/unicorn/unicorn_boss.nvgt:113:insert_last[0] +msg.93894d088f6a=- Use your axe to destroy a bridge support +; src/crafting/craft_buildings.nvgt:229:speak_with_history[0] +msg.93fa0af97c4f=There is already an herb garden in the base. +; src/crafting/craft_runes.nvgt:358:speak_with_history[0] +msg.93fc8e4d1331=Engraved {arg1} {arg2} with {arg3}. +; src/item_registry.nvgt:item_singular:stick +; seed:learn_sounds_label:sounds/items/stick.ogg +msg.946466f6c963=stick +; src/quests/enchanted_melody_game.nvgt:42:speak_with_history[0] +msg.94c221dc0888=Practice mode. Press Enter to begin, Escape to cancel. +; seed:learn_sounds_label:sounds/game/boar.ogg +msg.957f554c342a=boar +; src/world/world_snares.nvgt:98:notify[0] +msg.9612a65234f9={arg1} caught in snare at x {arg2} y 0! +; src/crafting/craft_barricade.nvgt:42:speak_with_history[0] +msg.96901dc80a5b=No materials to reinforce the barricade. +; src/menus/action_menu.nvgt:77:speak_with_history[0] +msg.96b0a63888d1=Cannot place snares in the base area. +; src/bosses/bandit_hideout.nvgt:853:insert_last[0] +msg.97c59037a63c=A survivor joins your base. +; src/world/world_snares.nvgt:146:speak_with_history[0] +msg.97dcc91b2ee2=You stepped on your snare and broke it! +; src/bosses/adventure_system.nvgt:46:speak_with_history[0] +msg.988157738a23=No adventures found in this area. +; src/fishing.nvgt:434:speak_with_history[0] +msg.98ae12aa6a63=You stop fishing. +; src/item_registry.nvgt:item_plural:smoked_fish +; src/item_registry.nvgt:item_singular:smoked_fish +msg.995d4e06ae4e=smoked fish +; excluded_sounds.nvgt:35:learn_sounds_add_description[1] +msg.998b61086fa7=You picked up an item for which there is no specific sound. +; src/quests/catch_the_boomerang_game.nvgt:9:insert_last[0] +msg.9afdb7e61391=- Press Space to throw +; src/bosses/adventure_system.nvgt:39:insert_last[0] +; src/bosses/bandit_hideout.nvgt:861:text_reader_lines[1] +msg.9b64062b2074=Bandit's Hideout +; src/enemies/undead.nvgt:308:speak_with_history[0] +msg.9b81947e1398=Residents killed an attacking {arg1}. +; seed:learn_sounds_label:sounds/enemies/bandit4.ogg +msg.9bbcfc6c8f9d=bandit4 +; seed:learn_sounds_label:sounds/quests/bone6.ogg +msg.9c19f78d931d=bone6 +; seed:learn_sounds_label:sounds/quests/bat2.ogg +msg.9dd40321998e=bat2 +; src/menus/action_menu.nvgt:252:insert_last[0] +msg.9ea4f66d0740=Check fire +; src/bosses/unicorn/unicorn_boss.nvgt:154:speak_with_history[0] +msg.9fcdb8881bce=You flee the encounter. +; src/quests/bat_invasion_game.nvgt:11:insert_last[0] +; src/quests/catch_the_boomerang_game.nvgt:8:insert_last[0] +; src/quests/escape_from_hel_game.nvgt:10:insert_last[0] +msg.a040b931b9bb=How to play: +; src/quests/skeletal_bard_game.nvgt:85:speak_with_history[0] +msg.a0844149a18c=You entered {arg1} notes and the actual number was {arg2}. {arg3} points. Press Enter to continue. +; seed:learn_sounds_label:sounds/terrain/deep_water.ogg +msg.a153dba5e67a=deep water +; seed:learn_sounds_label:sounds/nature/day.ogg +msg.a2620cbc10f5=day +; src/crafting/craft_buildings.nvgt:178:speak_with_history[0] +msg.a294ccba07b9=Firepit built here. +; src/bosses/adventure_system.nvgt:51:speak_with_history[0] +msg.a2cc4be07e5e=Adventure Menu. +; src/item_registry.nvgt:item_plural:small_game +; src/item_registry.nvgt:item_singular:small_game +msg.a2d7f5d60d98=small game +; seed:learn_sounds_label:sounds/items/miscellaneous.ogg +msg.a2f3a6c42a8f=miscellaneous +; src/menus/base_info.nvgt:48:insert_last[0] +msg.a3347af174f3=Pasture not built +; seed:learn_sounds_label:sounds/pets/wolf.ogg +msg.a3cb738850fa=wolf +; src/item_registry.nvgt:item_singular:skin_hat +msg.a3f2439ef01a=skin hat +; seed:learn_sounds_label:sounds/quests/bone1.ogg +msg.a56be51786dc=bone1 +; excluded_sounds.nvgt:43:learn_sounds_add_description[1] +; excluded_sounds.nvgt:44:learn_sounds_add_description[1] +; excluded_sounds.nvgt:45:learn_sounds_add_description[1] +msg.a570b024087d=Ever hungry, they feed upon your residents. +; src/quests/escape_from_hel_game.nvgt:15:insert_last[0] +msg.a641f39a6317=- The run ends when you fall into a grave +; src/bosses/bandit_hideout.nvgt:794:insert_last[0] +msg.a646f0dcb735=Favor awarded: {arg1}. +; src/time_system.nvgt:830:notify[0] +msg.a6b2cd34521f=The sun is setting. +; src/fishing.nvgt:263:speak_with_history[0] +msg.a6c587a6a19d=A fish is on the line! +; src/item_registry.nvgt:item_plural:slings +msg.a6caf0c3771d=slings +; src/quests/escape_from_hel_game.nvgt:5:insert_last[0] +msg.a6e2492f34b6==== Escape from Hel === +; src/crafting/craft_buildings.nvgt:339:speak_with_history[0] +msg.a75c9ab9b807=Stable already built. +; src/quests/bat_invasion_game.nvgt:15:insert_last[0] +msg.a79dc18e77d9=- You have 10 attempts +; libstorm-nvgt/notifications.nvgt:168:notifications_speak[0] +msg.a7cb7754af6c=Newest notification. {arg1} +; src/bosses/unicorn/unicorn_boss.nvgt:116:insert_last[0] +msg.a95eab2b45da=- Jump (UP arrow) to avoid being trampled +; src/crafting/craft_clothing.nvgt:289:speak_with_history[0] +msg.a9cf87f2edd7=Crafted {arg1} Skin Pants. +; seed:learn_sounds_label:sounds/enemies/goblin1.ogg +msg.aa3a1916166d=goblin1 +; src/item_registry.nvgt:item_singular:clay_pot +msg.aa5edb7494f3=clay pot +; src/item_registry.nvgt:item_singular:heal_scroll +; seed:learn_sounds_label:sounds/actions/heal_scroll.ogg +msg.ab2244bc6027=heal scroll +; src/base_system.nvgt:806:notify[0] +msg.abd02188f443=Resident retrieved {arg1} from snare at x {arg2} y 0 and reset it. +; seed:manual +msg.abe73973cc7c=Unable to load save. +; src/quests/enchanted_melody_game.nvgt:30:insert_last[0] +msg.ac36af678d78=Controls: +; draugnorak.nvgt:234:speak_with_history[0] +; src/bosses/bandit_hideout.nvgt:327:speak_with_history[0] +; src/bosses/unicorn/unicorn_boss.nvgt:140:speak_with_history[0] +msg.ac66e08b54d7=Resumed. +; src/pet_system.nvgt:608:speak_with_history[0] +msg.ac8ccb330272=A {arg1} joins you. +; src/item_registry.nvgt:item_singular:moccasin +msg.ad34f00c46cd=moccasin +; seed:learn_sounds_label:sounds/enemies/goblin_dies.ogg +msg.ad36d64ad3a3=goblin dies +; src/time_system.nvgt:712:speak_with_history[0] +msg.ad610e155539=The speed blessing fades. +; src/crafting/craft_tools.nvgt:375:speak_with_history[0] +msg.ade25255fbc1=Crafted a Canoe. +; src/time_system.nvgt:869:speak_with_history[0] +msg.ae534dc25ad3=Residents repaired the barricade. +{arg1} health. +; src/menus/altar_menu.nvgt:36:speak_with_history[0] +; src/menus/altar_menu.nvgt:64:speak_with_history[0] +msg.af1bd00fc4be=Nothing to sacrifice. +; src/crafting/craft_tools.nvgt:437:speak_with_history[0] +msg.af8df8d37140=Crafted {arg1} Canoes. +; src/item_registry.nvgt:item_plural:heal_scrolls +msg.af93482d73f0=heal scrolls +; seed:learn_sounds_label:sounds/pets/black_cat.ogg +msg.aff5100bebc5=black cat +; src/crafting/craft_weapons.nvgt:127:speak_with_history[0] +msg.b0b80b668953=Crafted {arg1} Spears. +; src/item_registry.nvgt:item_display:sticks +msg.b0cca9f9205a=Sticks +; src/item_registry.nvgt:item_display:vines +msg.b0f6aa37f763=Vines +; src/item_registry.nvgt:item_display:moccasins +msg.b11aa393c8a8=Moccasins +; src/crafting/craft_weapons.nvgt:146:speak_with_history[0] +msg.b23d59c4bbce=Crafted a Sling. +; seed:learn_sounds_label:sounds/actions/start_fishing.ogg +msg.b31837f2fc5b=start fishing +; src/quests/bat_invasion_game.nvgt:12:insert_last[0] +msg.b32fc272dc0d=- Listen for bats flying past from left or right +; src/base_system.nvgt:200:speak_with_history[0] +msg.b38e4cff23de=Resident caught a fish and added it to storage. +; src/menus/storage_menu.nvgt:213:speak_with_history[0] +; src/menus/storage_menu.nvgt:232:speak_with_history[0] +; src/menus/storage_menu.nvgt:275:speak_with_history[0] +msg.b46513dbc687=Withdrew {arg1} {arg2}. +; src/item_registry.nvgt:item_display:skin_tunics +msg.b48d0661f0da=Skin Tunics +; src/environment.nvgt:847:speak_with_history[0] +msg.b4a2314b1d80=This tree has been cut down. +; src/quests/skeletal_bard_game.nvgt:13:speak_with_history[0] +msg.b4a9d19a4e85=Select number of notes. {arg1}. +; seed:learn_sounds_label:sounds/enemies/bandit3.ogg +msg.b5d5cc084a95=bandit3 +; src/base_system.nvgt:240:speak_with_history[0] +msg.b5f1924def3d=Resident smoked a fish into {arg1} smoked fish. +; src/crafting/craft_barricade.nvgt:170:speak_with_history[0] +msg.b66ee1230dac=Reinforced barricade with stones. +{arg1} health. Now {arg2} of {arg3}. +; seed:learn_sounds_label:sounds/actions/call_pet.ogg +msg.b73a0709297f=call pet +; src/item_registry.nvgt:item_display:knives +msg.b774102c89b5=Knives +; src/enemies/bandit.nvgt:389:speak_with_history[0] +msg.b8de1d1a935f=Residents killed an attacking bandit. +; seed:learn_sounds_label:sounds/terrain/snow.ogg +msg.b94e9f3d7e00=snow +; src/bosses/bandit_hideout.nvgt:840:insert_last[0] +msg.ba3af68702a3=Learned Rune of Destruction! +; src/item_registry.nvgt:item_display:baskets_of_fruits_and_nuts +msg.ba7d7dfe330b=Baskets of Fruits and Nuts +; src/crafting/craft_clothing.nvgt:364:speak_with_history[0] +msg.bad2fcec0edf=Crafted moccasins. +; src/item_registry.nvgt:item_singular:basket_of_fruits_and_nuts +msg.bb0ea68db3b4=basket of fruits and nuts +; src/item_registry.nvgt:item_plural:meat +; src/item_registry.nvgt:item_singular:meat +msg.bb40f75a9c60=meat +; seed:learn_sounds_label:sounds/terrain/grass.ogg +msg.bbc29b76c59b=grass +; src/crafting/craft_tools.nvgt:504:speak_with_history[0] +msg.bbfd1384acaf=Crafted a clay pot. +; src/menus/action_menu.nvgt:145:speak_with_history[0] +msg.bd4796595f1f=No sticks to feed fire. +; src/menus/action_menu.nvgt:70:speak_with_history[0] +msg.bd573ed65ffe=No fish on the line. +; seed:learn_sounds_label:sounds/weapons/arrow_hit.ogg +msg.bdebda25dad9=arrow hit +; src/crafting/craft_clothing.nvgt:421:speak_with_history[0] +msg.bf57e001639c=Crafted a Skin Pouch. +; src/item_registry.nvgt:item_display:down +msg.bf93e5ce8bc1=Down +; src/crafting/craft_barricade.nvgt:152:speak_with_history[0] +msg.bfc7e2fc2280=Reinforced barricade with log. +{arg1} health. Now {arg2} of {arg3}. +; excluded_sounds.nvgt:26:learn_sounds_add_description[1] +msg.c01409b3a728=When casting release control when over water. When catching release when sound is over player. +; src/bosses/bandit_hideout.nvgt:817:insert_last[0] +msg.c05feaca7269=No items were recovered. +; src/item_registry.nvgt:item_singular:snare +msg.c1104d82c0bd=snare +; src/inventory_items.nvgt:470:speak_with_history[0] +; src/inventory_items.nvgt:487:speak_with_history[0] +; src/inventory_items.nvgt:491:speak_with_history[0] +msg.c2ff4bc695b7=Item not available. +; src/menus/equipment_menu.nvgt:64:speak_with_history[0] +msg.c3080306943b=Equipment menu. +; src/crafting/craft_materials.nvgt:96:speak_with_history[0] +; src/crafting/craft_materials.nvgt:146:speak_with_history[0] +msg.c3567877be0e=Crafted {arg1} arrows. +; src/crafting/craft_clothing.nvgt:138:speak_with_history[0] +msg.c418fde85a0d=Crafted a Skin Hat. +; src/inventory_items.nvgt:99:speak_with_history[0] +msg.c477acc3f80b=You can only carry {arg1} arrows with your current quivers. +; excluded_sounds.nvgt:37:learn_sounds_add_description[1] +msg.c48d642737b2=Inhabits mountainous regions. +; src/item_registry.nvgt:item_display:smoked_fish +msg.c4f7e9c21601=Smoked Fish +; excluded_sounds.nvgt:23:learn_sounds_add_description[1] +msg.c50641e24962=Your cast missed the water and landed in nearby foliage where it is unlikely to attract fish. +; src/enemies/bandit.nvgt:395:notify[0] +; src/enemies/undead.nvgt:314:notify[0] +msg.c50d661da7cf=The barricade has fallen! +; seed:learn_sounds_label:sounds/quests/fall.ogg +msg.c52888225c69=fall +; src/crafting/craft_buildings.nvgt:247:speak_with_history[0] +msg.c5565f654184=Herb garden built. The base now heals faster. +; src/bosses/unicorn/unicorn_boss.nvgt:175:speak_with_history[0] +msg.c6186586864e=x {arg1}, terrain {arg2}. Unicorn facing {arg3} +; seed:learn_sounds_label:sounds/enemies/bandit1.ogg +msg.c65e5dfc5ba7=bandit1 +; src/bosses/bandit_hideout.nvgt:785:insert_last[0] +msg.c72b7844929d=Bandits defeated: {arg1}. +; seed:learn_sounds_label:sounds/terrain/stream.ogg +msg.c82e3d7279ef=stream +; src/menus/base_info.nvgt:52:insert_last[0] +msg.c86ace9b97f8=Stable built. Horses {arg1} of {arg2} +; src/base_system.nvgt:202:speak_with_history[0] +msg.c8d882f9098c=Residents caught {arg1} fish and added them to storage. +; excluded_sounds.nvgt:33:learn_sounds_add_description[1] +msg.c8da1c4f1714=Lowers in pitch as the fall progresses. +; src/pet_system.nvgt:914:speak_with_history[0] +; src/pet_system.nvgt:937:speak_with_history[0] +msg.c8fee26f6ee3=Declined. +; src/item_registry.nvgt:item_display:meat +msg.c90399f41b3f=Meat +; seed:learn_sounds_label:sounds/quests/bone7.ogg +msg.c9532ab465fb=bone7 +; src/bosses/unicorn/unicorn_boss.nvgt:703:insert_last[0] +msg.cb6c212139a1=Learned Rune of Swiftness! +; src/bosses/bandit_hideout.nvgt:804:insert_last[0] +msg.cb8b9677952a=Storage rewards: +; src/item_registry.nvgt:item_plural:boar_carcasses +msg.cb8e70ab2bcc=boar carcasses +; src/item_registry.nvgt:item_singular:bowstring +msg.cc167fb645e4=bowstring +; src/menus/action_menu.nvgt:175:speak_with_history[0] +msg.cc3caa9812e2=No logs to feed fire. +; src/item_registry.nvgt:item_plural:reed_baskets +msg.cc61d0f3de65=reed baskets +; src/crafting/craft_buildings.nvgt:188:speak_with_history[0] +msg.ccb9a6240155=You need a firepit within 2 tiles to build a fire. +; seed:learn_sounds_label:sounds/weapons/axe_hit.ogg +msg.cccc5c842adb=axe hit +; src/crafting/craft_buildings.nvgt:309:speak_with_history[0] +msg.ccce38999a27=Pasture already built. +; src/time_system.nvgt:720:speak_with_history[0] +msg.cd13e6ce9297=The eagle's sight fades. +; src/menus/storage_menu.nvgt:106:speak_with_history[0] +; src/menus/storage_menu.nvgt:126:speak_with_history[0] +; src/menus/storage_menu.nvgt:161:speak_with_history[0] +msg.cd3ef7da3f4f=Deposited {arg1} {arg2}. +; draugnorak.nvgt:277:speak_with_history[0] +; src/menus/menu_utils.nvgt:35:speak_with_history[0] +msg.cd4c1cc68057=Burning! {arg1} health remaining. +; seed:learn_sounds_label:sounds/enemies/enter_range.ogg +msg.ce13e4341cdc=enter range +; src/menus/inventory_core.nvgt:25:insert_last[0] +; src/menus/inventory_core.nvgt:41:insert_last[0] +; src/menus/storage_menu.nvgt:287:insert_last[0] +msg.ce3897c6b3be={arg1}: {arg2} +; seed:learn_sounds_label:sounds/items/item_breaks.ogg +msg.ce5aeca95fb9=item breaks +; src/i18n.nvgt:669:speak_with_history[0] +; src/pet_system.nvgt:906:speak_with_history[0] +; src/save_system.nvgt:344:speak_with_history[0] +msg.cea88b214070={arg1} {arg2} +; seed:manual +msg.cfe2650b4c6e=Found a stone. +; src/bosses/unicorn/unicorn_boss.nvgt:119:insert_last[0] +msg.d04a59a6a71b=Controls: LEFT/RIGHT to move, UP to jump, CTRL to attack, ESC to flee +; src/item_registry.nvgt:item_singular:skin +msg.d0cb2acd0739=skin +; src/menus/altar_menu.nvgt:52:speak_with_history[0] +msg.d103663bcfd9=Sacrificed 1 {arg1}. Favor +{arg2}. Total {arg3}. +; src/crafting/craft_barricade.nvgt:116:speak_with_history[0] +msg.d1c5baa8a36b=Reinforced barricade with sticks. +{arg1} health. Now {arg2} of {arg3}. +; seed:learn_sounds_label:sounds/enemies/enemy_falls.ogg +msg.d34b6217ea75=enemy falls +; src/item_registry.nvgt:item_display:skin_gloves +msg.d373510877fd=Skin Gloves +; draugnorak.nvgt:587:ui_info_box[1] +; seed:manual +msg.d39008c4a392=Unhandled exception +; src/item_registry.nvgt:item_singular:backpack +msg.d49f2e760778=backpack +; src/quests/enchanted_melody_game.nvgt:33:insert_last[0] +msg.d537941485fb=R or I - Third note +; src/crafting/craft_buildings.nvgt:214:speak_with_history[0] +msg.d5dd6ddda4ef=Fire built at firepit. +; src/menus/equipment_menu.nvgt:34:speak_with_history[0] +msg.d5e86e1918e8=Nothing to equip. +; src/crafting/craft_buildings.nvgt:331:speak_with_history[0] +msg.d5e89cddb940=Stable must be built in the base. +; draugnorak.nvgt:187:ui_info_box[2] +; seed:manual +msg.d609f615ddae=No saves found. +; draugnorak.nvgt:418:speak_with_history[0] +; src/bosses/bandit_hideout.nvgt:395:speak_with_history[0] +; src/bosses/unicorn/unicorn_boss.nvgt:226:speak_with_history[0] +msg.d63eba28afc0=west +; seed:learn_sounds_label:sounds/enemies/invasion.ogg +msg.d6dc139f529a=invasion +; seed:learn_sounds_label:sounds/enemies/yeti1.ogg +msg.d6fd3467a950=yeti1 +; src/item_registry.nvgt:item_display:axes +msg.d75f36f88797=Axes +; src/bosses/bandit_hideout.nvgt:832:insert_last[0] +msg.d7879bd506af=No livestock were recovered. +; src/time_system.nvgt:650:notify[0] +msg.d8ea835664e1=Your base is at maximum capacity. +; src/quests/enchanted_melody_game.nvgt:31:insert_last[0] +msg.da0b5a461d98=F or K - First note (lowest pitch) +; seed:learn_sounds_label:sounds/weapons/sling_hit.ogg +msg.da11dc5e090c=sling hit +; seed:learn_sounds_label:sounds/enemies/vampyr_dies.ogg +msg.da93fb9992a3=vampyr dies +; src/fishing.nvgt:331:speak_with_history[0] +msg.dadcebf86f43=Caught a {arg1} {arg2}. +; excluded_sounds.nvgt:47:learn_sounds_add_description[1] +msg.dadd68431e60=Inhabits snowy regions. +; seed:learn_sounds_label:sounds/enemies/exit_range.ogg +msg.db2a8514a0fc=exit range +; src/quest_system.nvgt:204:speak_with_history[0] +msg.db476a28fe5b=Quest menu. +; src/bosses/bandit_hideout.nvgt:309:text_reader_lines[1] +; src/bosses/unicorn/unicorn_boss.nvgt:121:text_reader_lines[1] +msg.db56e2e1db9f=Adventure +; seed:learn_sounds_label:sounds/enemies/bandit2.ogg +msg.db5bbabf1bc3=bandit2 +; draugnorak.nvgt:70:speak_with_history[0] +msg.dbea5ffbac7b=Draugnorak. Main menu. +; src/menus/action_menu.nvgt:346:speak_with_history[0] +msg.dc5c17f80fbe=Can't do that. +; src/fylgja_system.nvgt:115:speak_with_history[0] +msg.ddea499514d8=You have already used your Fylgja today. +; src/item_registry.nvgt:item_display:sinew +msg.ddf360815408=Sinew +; src/bosses/bandit_hideout.nvgt:307:insert_last[0] +msg.ddfc9c735c48=Bandits will, of course, not take this lying down. +; src/item_registry.nvgt:item_plural:baskets_of_fruits_and_nuts +msg.de0dce8ef11c=baskets of fruits and nuts +; src/menus/base_info.nvgt:38:insert_last[0] +msg.de2c51fece28=Altar not built +; seed:learn_sounds_label:sounds/quests/footstep.ogg +msg.de4e8495f978=footstep +; src/item_registry.nvgt:item_plural:clay +; src/item_registry.nvgt:item_singular:clay +msg.de6b29cb1808=clay +; src/item_registry.nvgt:item_plural:feathers +msg.de752f6fdd8b=feathers +; src/quests/skeletal_bard_game.nvgt:44:insert_last[0] +msg.df3f34f3f144=A skeleton named Billy Bones is practicing to become a bard. +; src/bosses/bandit_hideout.nvgt:304:insert_last[0] +msg.df63c8846d76=Objective: +; src/crafting/craft_clothing.nvgt:459:speak_with_history[0] +msg.df93ea08d751=Crafted {arg1} Skin Pouches. +; src/crafting/craft_clothing.nvgt:524:speak_with_history[0] +msg.dfa22c1cada0=Crafted {arg1} Backpacks. +; src/menus/altar_menu.nvgt:81:speak_with_history[0] +msg.dffaa4b11d31=Sacrificed {arg1} {arg2}. Favor +{arg3}. Total {arg4}. +; src/item_registry.nvgt:item_display:clay +msg.e00d5664a78e=Clay +; src/bosses/unicorn/unicorn_boss.nvgt:109:insert_last[0] +msg.e0b678c82a60=A wooden bridge spans a deep chasm ahead. The bridge has two supports +; src/crafting/craft_runes.nvgt:163:speak_with_history[0] +msg.e0c7f029cfa2=No runes unlocked yet. +; src/bosses/unicorn/unicorn_boss.nvgt:112:insert_last[0] +msg.e142524d7689=Strategy: +; libstorm-nvgt/learn_sounds.nvgt:363:learn_sounds_speak[0] +msg.e1c16d0490ee=No sounds available. +; src/crafting/craft_tools.nvgt:243:speak_with_history[0] +msg.e1dc1099a799=Crafted {arg1} Fishing Poles. +; src/item_registry.nvgt:item_singular:skin_tunic +msg.e253d297aabc=skin tunic +; src/item_registry.nvgt:item_display:fishing_poles +msg.e2e91797da96=Fishing Poles +; src/item_registry.nvgt:item_singular:stone +; seed:learn_sounds_label:sounds/items/stone.ogg +; seed:learn_sounds_label:sounds/terrain/stone.ogg +msg.e30bfd0c38dc=stone +; src/crafting/craft_tools.nvgt:259:speak_with_history[0] +msg.e31811cdce94=Crafted rope. +; seed:learn_sounds_label:sounds/actions/bad_cast.ogg +msg.e34161b98188=bad cast +; src/bosses/unicorn/unicorn_boss.nvgt:700:insert_last[0] +msg.e379741bfbd3=Heal Scrolls: +{arg1}. +; src/item_registry.nvgt:item_display:quivers +msg.e3ce3e543239=Quivers +; seed:learn_sounds_label:sounds/weapons/sling_high_range.ogg +msg.e3d828480491=sling high range +; src/quests/bat_invasion_game.nvgt:17:insert_last[0] +; src/quests/catch_the_boomerang_game.nvgt:14:insert_last[0] +; src/quests/escape_from_hel_game.nvgt:17:insert_last[0] +msg.e4427f09bc09=Close this screen to begin. +; src/menus/action_menu.nvgt:237:insert_last[0] +msg.e5a51ddbb159=Feed fire with vine +; seed:learn_sounds_label:sounds/enemies/undead_resident1.ogg +msg.e5d79aba2048=undead resident1 +; seed:learn_sounds_label:sounds/enemies/zombie_hit.ogg +msg.e5f656d506a7=zombie hit +; src/quests/escape_from_hel_game.nvgt:8:insert_last[0] +msg.e6526216c064=You have plundered the treasure, and the Draugr are displeased.! +; src/bosses/bandit_hideout.nvgt:843:insert_last[0] +msg.e65fa8960222=You have already mastered the Rune of Destruction. +; seed:learn_sounds_label:sounds/quests/bone8.ogg +msg.e6ae58426f13=bone8 +; seed:learn_sounds_label:sounds/actions/feed_fire.ogg +msg.e7409fcb54bc=feed fire +; src/environment.nvgt:775:speak_with_history[0] +msg.e74f453392b6=Collected snare. +; src/crafting/craft_barricade.nvgt:134:speak_with_history[0] +msg.e75d3068a5e9=Reinforced barricade with vines. +{arg1} health. Now {arg2} of {arg3}. +; seed:learn_sounds_label:sounds/terrain/wood.ogg +msg.e8002c169040=wood +; src/quests/escape_from_hel_game.nvgt:13:insert_last[0] +msg.e8216720d30b=- Press SPACE to jump over graves +; draugnorak.nvgt:89:insert_last[0] +msg.e8db3a42f86f=Learn Sounds +; src/bosses/bandit_hideout.nvgt:841:insert_last[0] +msg.e8e8f4ee7a5a=You can now engrave weapons with this rune at the crafting menu. +; src/crafting/craft_barricade.nvgt:145:speak_with_history[0] +msg.e8fe153261a9=Not enough logs. +; src/bosses/bandit_hideout.nvgt:302:insert_last[0] +msg.e9dd1d268b65=The base lies far to the east, guarded by a barricade. +; src/bosses/unicorn/unicorn_boss.nvgt:211:speak_with_history[0] +msg.ea6bfdb7bd29=The Unicorn trampled you. +; src/menus/base_info.nvgt:36:insert_last[0] +msg.ea771032e597=Altar built +; src/fishing.nvgt:161:speak_with_history[0] +msg.ead6f13efc8d=You need a fishing pole equipped. +; src/crafting/craft_tools.nvgt:148:speak_with_history[0] +msg.eb9cac43e997=Crafted a Snare. +; libstorm-nvgt/notifications.nvgt:153:notifications_speak[0] +; libstorm-nvgt/notifications.nvgt:173:notifications_speak[0] +msg.ebf229b227f8={arg1} {arg2} of {arg3} +; src/fylgja_system.nvgt:182:speak_with_history[0] +msg.ec465c0c4000=You need open ground in front of you to charge. +; src/base_system.nvgt:1082:speak_with_history[0] +msg.eced46d198ed=Resident gathered a basket of fruits and nuts. +; src/environment.nvgt:1191:speak_with_history[0] +msg.ed989350becb=Reached elevation {arg1}. +; draugnorak.nvgt:289:speak_with_history[0] +; src/menus/menu_utils.nvgt:46:speak_with_history[0] +msg.edbcc0a199d1={arg1} health. +; src/world/world_snares.nvgt:144:speak_with_history[0] +msg.ee02d6d4c199=You stepped on your snare! The {arg1} escaped. +; src/item_registry.nvgt:item_display:spears +msg.ee200b3d8ae2=Spears +; seed:learn_sounds_label:sounds/bosses/unicorn/unicorn_on_bridge.ogg +msg.eff5a5f34d9a=unicorn on bridge +; seed:learn_sounds_label:sounds/weapons/bow_draw.ogg +msg.f015f25c409b=bow draw +; src/quests/enchanted_melody_game.nvgt:75:speak_with_history[0] +msg.f08ee9fe3340=Starting. Repeat the pattern. +; src/item_registry.nvgt:item_display:ropes +msg.f12b2d219fe8=Ropes +; src/time_system.nvgt:469:speak_with_history[0] +msg.f137213e72e2=A swarm of zombies has been spotted. +; excluded_sounds.nvgt:38:learn_sounds_add_description[1] +msg.f16169ef79aa=Not quite a zombie nor a vampyr. These undead remember they used to have a home, and "\n "maybe there's food there... +; src/menus/base_info.nvgt:62:speak_with_history[0] +msg.f1c671ae3045=Base info. No options. +; src/environment.nvgt:29:speak_with_history[0] +msg.f229791ba917=Fell {arg1} feet! Took {arg2} damage. {arg3} health remaining. +; src/quests/catch_the_boomerang_game.nvgt:4:insert_last[0] +msg.f305da2c3550==== Catch the Boomerang === +; draugnorak.nvgt:334:speak_with_history[0] +msg.f32ea7a06f91={arg1}, x {arg2}, y {arg3}, terrain {arg4} +; seed:learn_sounds_label:sounds/nature/wind_low.ogg +msg.f3c5ed579a54=wind low +; seed:learn_sounds_label:sounds/player_female_damage.ogg +msg.f3d964225e83=player female damage +; src/crafting/craft_buildings.nvgt:323:speak_with_history[0] +msg.f3e51f53f970=Pasture built. +; src/crafting/craft_barricade.nvgt:258:speak_with_history[0] +msg.f40ad52f4d97=Reinforced barricade {arg1} times with log. Health now {arg2}. +; src/crafting/craft_materials.nvgt:427:speak_with_history[0] +msg.f48b940f83c7=Butchered {arg1}. Got 1 meat and 1 skin. +; src/crafting/craft_materials.nvgt:414:speak_with_history[0] +msg.f50492d2703a=Butchered goose. Got 1 meat, feathers, and down. +; src/item_registry.nvgt:item_singular:reed +msg.f5cabf873590=reed +; src/base_system.nvgt:1006:speak_with_history[0] +msg.f5f6c13f2ceb=Resident added {arg1} to storage. +; src/bosses/bandit_hideout.nvgt:301:insert_last[0] +msg.f77fc018cb7b=You find a hidden bandit base deep in the forest. +; draugnorak.nvgt:105:insert_last[0] +msg.f83b6fe3aebf=Exit +; src/inventory_items.nvgt:458:speak_with_history[0] +; src/quests/skeletal_bard_game.nvgt:24:speak_with_history[0] +; src/quests/skeletal_bard_game.nvgt:30:speak_with_history[0] +msg.f85b4b604c9b={arg1}. +; seed:learn_sounds_label:sounds/enemies/undead_resident_dies.ogg +msg.f8909d26fe86=undead resident dies +; src/crafting/craft_weapons.nvgt:203:speak_with_history[0] +msg.f8b2a45e7e4b=Crafted a Bow. +; seed:manual +msg.f8fc573e2d2f=Load Game (no saves found) +; src/item_registry.nvgt:item_singular:bow +msg.fa5bd38090c7=bow +; draugnorak.nvgt:195:speak_with_history[0] +msg.fa87b71ab25a=Game loaded. +; src/crafting/craft_buildings.nvgt:196:speak_with_history[0] +msg.fa88a4c8033c=There is already a fire in the base. +; src/crafting/craft_clothing.nvgt:252:speak_with_history[0] +msg.fb091149bb52=Crafted Skin Pants. +; src/quest_system.nvgt:72:notify[0] +msg.fb09c6d50b11=A new quest is available: {arg1}. +; src/crafting/craft_buildings.nvgt:293:speak_with_history[0] +msg.fb389469701e=Storage upgraded. Capacity is now {arg1} per item. +; seed:learn_sounds_label:sounds/bosses/unicorn/unicorn_galloping.ogg +msg.fb4a022a82b3=unicorn galloping +; seed:learn_sounds_label:sounds/nature/rain.ogg +msg.fbec17cb2fcb=rain +; src/menus/action_menu.nvgt:284:speak_with_history[0] +; src/menus/altar_menu.nvgt:113:speak_with_history[0] +; src/menus/altar_menu.nvgt:153:speak_with_history[0] +msg.fc1740dbb4ae=No matches for {arg1}. +; src/crafting/craft_runes.nvgt:216:insert_last[0] +msg.fc1ad6e10ac4={arg1} ({arg2} available) +; src/crafting/craft_materials.nvgt:203:speak_with_history[0] +msg.fc5294f9b150=Crafted {arg1} Bowstrings. +; src/item_registry.nvgt:item_display:heal_scrolls +msg.fca1af3d07b0=Heal Scrolls +; libstorm-nvgt/volume_controls.nvgt:71:screen_reader_speak[0] +msg.fd1a25749457=Volume {arg1}. +; src/menus/action_menu.nvgt:108:speak_with_history[0] +msg.fdd0b57cb7b9=You toss a fiew vines and leaves into the fire. +; src/crafting/craft_buildings.nvgt:259:speak_with_history[0] +msg.fe0ede7d0360=Storage is fully upgraded. +; seed:learn_sounds_label:sounds/menu/Draugnorak.ogg +; seed:learn_sounds_label:sounds/menu.bak/Draugnorak.ogg +msg.fe2a9d6606e0=draugnorak +; src/quests/bat_invasion_game.nvgt:21:speak_with_history[0] +msg.fe406c6e0055=Starting. +; src/bosses/unicorn/unicorn_boss.nvgt:104:insert_last[0] +msg.feea4a3b0a84==== Unicorn Hunt === +; src/quests/enchanted_melody_game.nvgt:32:insert_last[0] +msg.fefc084f67de=D or J - Second note +; src/item_registry.nvgt:item_plural:bows +msg.ff5ac0b6df7b=bows +; src/crafting/craft_materials.nvgt:308:speak_with_history[0] +msg.ffed2db61296=Smoked a fish into {arg1} smoked fish. diff --git a/lang/es.ini b/lang/es.ini new file mode 100644 index 0000000..fbc2f2e --- /dev/null +++ b/lang/es.ini @@ -0,0 +1,802 @@ +; Draugnorak localization catalog +; Copy this file to lang/.ini and translate only the right-hand values. +; Keep keys unchanged. Use placeholders like {arg1}, {count}, {language} exactly as written. + +[meta] +code=es +name=Spanish +native_name=Espanol + +[system] + +[messages] +language.select_prompt=Selecciona tu idioma. +language.selected=Idioma establecido en {language}. +language.english_label=English (en) +ui.window_title=Draugnorak +sex.male=Masculino +sex.female=Femenino +new_character.choose_sex=Elige tu sexo. +new_character.enter_name=Ingresa tu nombre o presiona Enter para un nombre aleatorio. +new_character.save_exists_overwrite=Se encontro una partida para {name}. Sobrescribir? +load_game.option_with_metadata={name}, {sex}, dia {day} +load_game.delete_confirm_base=Seguro que quieres eliminar el personaje {name}? +load_game.delete_confirm_with_metadata=Seguro que quieres eliminar el personaje {name} sexo {sex} dia {day}? +load_game.delete_save_heading=Eliminar partida +load_game.delete_save_failed=No se pudo eliminar la partida. +quick_slot.no_item_bound=No hay ningun objeto asignado a la ranura {slot}. +menu.closed=Cerrado. +menu.canceled=Cancelado. +menu.no_options=Sin opciones. +menu.no_matches=Sin coincidencias para {arg1}. +option.no=No +option.yes=Si +inventory.cant_carry_any_more_item=No puedes llevar mas {item}. +inventory.menu.prompt=Menu de inventario. {option} +inventory.menu.no_options=Menu de inventario. Sin opciones. +inventory.option.personal=Inventario personal +inventory.option.base_storage=Almacen base +inventory.count_set_to_slot=Conteo de {name} asignado a la ranura {slot}. +inventory.need_quiver_for_arrows=Necesitas un carcaj para llevar flechas. +search.found_item=Encontraste {item}. +search.found_nothing=No encontraste nada. +pickup.item=Recogiste {item}. +storage.window_title=Inventario +storage.transfer_prompt={prompt} (maximo {max}) +storage.deposit_how_many=Cuantos depositar? +storage.withdraw_how_many=Cuantos retirar? +storage.nothing_to_deposit=Nada para depositar. +storage.nothing_to_withdraw=Nada para retirar. +storage.runed_cannot_deposit=Los objetos rúnicos no se pueden depositar en el almacen. +storage.item_full=El almacenamiento para ese objeto esta lleno. +storage.no_storage_built=No hay almacen construido. +storage.menu_title=Almacen base. +storage.menu.prompt=Almacen base. {option} +storage.menu.no_options=Almacen base. Sin opciones. +crafting.require.fire_within_three_clay_pot=Necesitas un fuego a menos de 3 casillas para fabricar una olla de arcilla. +crafting.require.fire_within_three_clay_pots=Necesitas un fuego a menos de 3 casillas para fabricar ollas de arcilla. +crafting.require.fire_within_three_bowstring=Necesitas un fuego a menos de 3 casillas para hacer cuerda de arco. +crafting.require.altar_incense=Necesitas un altar para fabricar incienso. +crafting.require.fire_within_three_smoke_fish=Necesitas un fuego a menos de 3 casillas para ahumar pescado. +crafting.require.fire_within_three_butcher=Necesitas un fuego a menos de 3 casillas para descuartizar. +character.slot.head=cabeza +character.slot.torso=torso +character.slot.arms=brazos +character.slot.hands=manos +character.slot.legs=piernas +character.slot.feet=pies +character.menu.prompt=Informacion del personaje. {option} +character.menu.no_options=Informacion del personaje. Sin opciones. +character.pet.no_pet=No tienes mascota. +character.pet.abandon_confirm=Seguro que quieres abandonar a tu mascota? {option} +character.pet.unconscious_cannot_abandon=Tu {pet} esta inconsciente. No puedes abandonarla ahora. +item.label.items=objetos +item.label.item=objeto +item.label.unknown=Desconocido +equipment.menu.equipped_suffix=(equipado) +equipment.name.none=Ninguno +equipment.name.unknown=Desconocido +equipment.name.spear=Lanza +equipment.name.stone_axe=Hacha de piedra +equipment.name.sling=Honda +equipment.name.bow=Arco +equipment.name.skin_hat=Sombrero de piel +equipment.name.skin_gloves=Guantes de piel +equipment.name.skin_pants=Pantalones de piel +equipment.name.skin_tunic=Tunica de piel +equipment.name.moccasins=Mocasines +equipment.name.skin_pouch=Bolsa de piel +equipment.name.backpack=Mochila +equipment.name.fishing_pole=Cana de pescar +equipment.name_plural.spears=Lanzas +equipment.name_plural.stone_axes=Hachas de piedra +equipment.name_plural.slings=Hondas +equipment.name_plural.bows=Arcos +equipment.name_plural.skin_hats=Sombreros de piel +equipment.name_plural.skin_gloves=Guantes de piel +equipment.name_plural.skin_pants=Pantalones de piel +equipment.name_plural.skin_tunics=Tunicas de piel +equipment.name_plural.moccasins=Mocasines +equipment.name_plural.skin_pouches=Bolsas de piel +equipment.name_plural.backpacks=Mochilas +equipment.name_plural.fishing_poles=Canas de pescar +equipment.name_plural.items=Objetos +environment.tree.fell=El arbol cayo! +environment.tree.fell_with_loot=El arbol cayo! Obtuviste {sticks} {sticks_label}, {vines} {vines_label} y {logs} {logs_label}. +environment.tree.inventory_full=Inventario lleno. +crafting.menu.prompt=Menu de fabricacion. {option} +crafting.category.weapons=Armas +crafting.category.tools=Herramientas +crafting.category.materials=Materiales +crafting.category.clothing=Ropa +crafting.category.buildings=Edificios +crafting.category.barricade=Barricada +crafting.category.runes=Runas +crafting.weapons.prompt=Armas. {option} +crafting.weapons.option.spear=Lanza (1 Palo, 1 Liana, 1 Piedra) [Requiere Cuchillo] +crafting.weapons.option.sling=Honda (1 Piel, 2 Lianas) +crafting.weapons.option.bow=Arco (1 Palo, 1 Cuerda de arco) +crafting.tools.prompt=Herramientas. {option} +crafting.tools.option.stone_knife=Cuchillo de piedra (2 Piedras) +crafting.tools.option.snare=Trampa (1 Palo, 2 Lianas) +crafting.tools.option.stone_axe=Hacha de piedra (1 Palo, 1 Liana, 2 Piedras) [Requiere Cuchillo] +crafting.tools.option.fishing_pole=Cana de pescar (1 Palo, 2 Lianas) +crafting.tools.option.rope=Cuerda (3 Lianas) +crafting.tools.option.quiver=Carcaj (2 Pieles, 2 Lianas) +crafting.tools.option.canoe=Canoa (4 Troncos, 11 Palos, 11 Lianas, 6 Pieles, 2 Cuerdas, 6 Juncos) +crafting.tools.option.reed_basket=Cesta de junco (3 Juncos) +crafting.tools.option.clay_pot=Olla de arcilla (3 Arcilla) +crafting.materials.prompt=Materiales. {option} +crafting.materials.option.butcher_game=Descuartizar caza [Requiere Caza, Cuchillo y Fuego cercano] +crafting.materials.option.smoke_fish=Ahumar pescado (1 Pescado, 1 Palo) [Requiere Fuego cercano] +crafting.materials.option.arrows=Flechas (2 Palos, 4 Plumas, 2 Piedras) [Requiere Carcaj] +crafting.materials.option.bowstring=Cuerda de arco (3 Tendones) [Requiere Fuego cercano] +crafting.materials.option.incense=Incienso (6 Palos, 2 Lianas, 1 Junco) [Requiere Altar] +crafting.clothing.prompt=Ropa. {option} +crafting.clothing.option.skin_hat=Sombrero de piel (1 Piel, 1 Liana) +crafting.clothing.option.skin_gloves=Guantes de piel (1 Piel, 1 Liana) +crafting.clothing.option.skin_pants=Pantalones de piel (6 Pieles, 3 Lianas) +crafting.clothing.option.skin_tunic=Tunica de piel (4 Pieles, 2 Lianas) +crafting.clothing.option.moccasins=Mocasines (2 Pieles, 1 Liana) +crafting.clothing.option.skin_pouch=Bolsa de piel (2 Pieles, 1 Liana) +crafting.clothing.option.backpack=Mochila (11 Pieles, 5 Lianas, 4 Bolsas de piel) +crafting.buildings.prompt=Edificios. {option} +crafting.buildings.option.firepit=Fogata (9 Piedras) +crafting.buildings.option.fire=Fuego (2 Palos, 1 Tronco) [Requiere Fogata] +crafting.buildings.option.herb_garden=Huerto de hierbas (9 Piedras, 3 Lianas, 2 Troncos) [Solo base] +crafting.buildings.option.storage_upgrade_1=Mejorar almacen (6 Troncos, 9 Piedras, 8 Lianas) [Solo base, 50 cada] +crafting.buildings.option.storage_upgrade_2=Mejorar almacen (12 Troncos, 18 Piedras, 16 Lianas) [Solo base, 100 cada] +crafting.buildings.option.pasture=Corral (8 Troncos, 18 Cuerdas) [Solo base, Requiere mejora de almacen] +crafting.buildings.option.stable=Establo (10 Troncos, 15 Piedras, 10 Lianas) [Solo base, Requiere mejora de almacen] +crafting.buildings.option.altar=Altar (9 Piedras, 3 Palos) [Solo base] +crafting.barricade.prompt=Barricada. {option} +crafting.barricade.option.reinforce_sticks=Reforzar con palos ({cost} palos, +{health} de salud) +crafting.barricade.option.reinforce_vines=Reforzar con lianas ({cost} lianas, +{health} de salud) +crafting.barricade.option.reinforce_log=Reforzar con tronco ({cost} tronco, +{health} de salud) +crafting.barricade.option.reinforce_stones=Reforzar con piedras ({cost} piedras, +{health} de salud) +crafting.missing=Falta: {requirements} +crafting.requirement.stone_knife=Cuchillo de piedra +crafting.requirement.game=Caza +crafting.requirement.favor=favor +crafting.requirement.resources=recursos +msg.00884428322b=foso +msg.01a159425acf=romper trampa +msg.01af99b118c8={arg1} Fylgja +msg.03268c7d7239=mochilas +msg.03976920927e=cuerda +msg.03ff0c460a1f=Sin opciones. +msg.0406529811c7=caza menor +msg.041d8413712d=Se escribio un registro de error en tu carpeta de datos guardados. +msg.04a8916a9494=vampiro1 +msg.0532db2192fa=incienso +msg.05c95b66bd57=El cuchillo de un residente se rompio mientras descuartizaba. +msg.066723a92e94==== Victoria Recompensas === +msg.076a11174ca0=No hay espacio para resultados. +msg.07e9ab246cdd=Despues de cerrar esta pantalla, puedes practicar las notas. +msg.08b42842d5bc=tendon +msg.093440631290=Ya dominaste la Runa de Celeridad. +msg.095b4ad6f082=Los objetos rúnicos no se pueden sacrificar. +msg.096539f23463=Retumban los tambores de guerra! Preparate para el combate! +msg.09c28ae8f83b=Piedras +msg.09d016686864=Establo no construido +msg.0a133b0ecad2=Fabricaste un Pesca Caña. +msg.0a3bdcef1906=Cestas de junco +msg.0a9d6438c818==== Melodia Encantada === +msg.0addf2ff4572=Quemado {arg1} incienso. +{arg2} horas. +msg.0b3d8d5cdca6=- Llega a la base y destruye la barricada +msg.0b78c6a3e407=El corral debe construirse en la base. +msg.0ba82f2cdb6c=Tu mascota es en its way. +msg.0bcd9af79f2d=bosque +msg.0c7789cf9a52=fabricacion completa +msg.0c976639507d=Alimentar fuego con palo +msg.0cb01a9fb609=Fabricaste un Piedra Hacha. +msg.0cf604cb001b=Flechas +msg.0d1ec2e04286=honda golpe +msg.0da18cb6fbe2=Fabricaste un Mochila. +msg.0e16a11cda9a=cuchillos +msg.0e2e63241760=El almacen debe construirse en la base. +msg.0e57fb642209=lanza +msg.0eb796e65c39=Hondas +msg.0f5211918a6f=Caza del Unicornio (Jefe de montana) +msg.0f9af12cbb82=piel bolsa +msg.10065b4cd62c=Colocar Trampa +msg.10adaed44a3e=Ya hay una trampa aqui. +msg.114e55e657be=Tu arrojar un arm cargar de palos en el fuego. +msg.118e74882896=El huerto de hierbas solo puede construirse en la zona de base. +msg.125769e87318=No enough lianas. +msg.126dd3b70a5c=Troncos +msg.131bd02673e5=Menu de acciones. +msg.136e4cb03bd5=No se pudo guardar el archivo +msg.13cd01ceccd3=Arrojaste {arg1} lianas en el fuego. +msg.14056a0402ef=Tu {arg1} se ha recuperado de sus heridas. +msg.14272bbf4f20=No hay fuego cerca. +msg.145ba8a7a0f2=carcajes +msg.14f5f5ff4f76=No se encontraron supervivientes. +msg.15e5059dafbc=No hay suficiente capacidad de carcaj para flechas. +msg.16134a4250f7=caza cae +msg.162bb07757cf=Establo o almacen no construido. No se capturaron caballos. +msg.16a233f44489=- Escucha las tumbas abiertas acercandose (cada vez mas fuerte) +msg.16cd07c0a9b6=Reforzaste la barricada {arg1} veces con palos. Salud ahora {arg2}. +msg.16d29946de8e={arg1} equipado. +msg.17213930ddb9=El fuego en x {arg1} y {arg2} se esta quedando bajo! +msg.176b9a0c8398=Seguro que quieres salir? +msg.17808e3775e1=incienso palo +msg.1792abbb7eb8=Selecciona equipo a grabar con {arg1}. {arg2} +msg.17b5b2a286cc==== Bardo Esqueletico === +msg.17c677526bd6=Presiona Space para lanzar. +msg.18b837bb6f43=murcielago1 +msg.18c77e824a2a=yeti muere +msg.1a25b0443252=Descuartizaste jabali. Obtuviste carne, 3 pieles y 2 tendon. +msg.1a853cbfbc36=bandido muere +msg.1af5fdafa634=Fabricacion... +msg.1bb8ef2886a3=No muerto basico. +msg.1be2a44cb53d=noche +msg.1bfa6bc809d4==== Escondite de Bandidos === +msg.1c2272bc0140=Tu caiste en. Puntuacion {arg1}. +msg.1c5cbc1424ec=No enough piedras. +msg.1c7022965d1a=piel guante +msg.1d15d0d5c98d=Almacen construido. Total objetos {arg1} +msg.1d493d7011a0=No hay lianas para alimentar el fuego. +msg.1d61343b9660=Almacen no construido. Sin recompensas de objetos. +msg.1da840244989=fuego +msg.1db76ac85135=honda +msg.20a5cf945886=Tu {arg1} esta inconsciente. No puedes abandonarlo ahora. +msg.21192381c66d=UN {arg1} escapo desde tu trampa en x {arg2} y 0! +msg.21b534113694=Recompensas de mision +msg.226f7d1cdeb1=Revisar pesca caña +msg.237b5017397b=hacha +msg.244b7f948d5d=La invasion de {arg1} ha terminado. +msg.2455422b697c=Ya intentaste una aventura hoy. +msg.245c89590526=- Cuando el puente colapse con el Unicornio encima, ganas! +msg.247e80c6393a=Has muerto. +msg.24d4bced9a39=Bajando. +msg.25038d9da464=este +msg.25296a531b53=Victoria! +msg.25db7a4ec692=Se recupero ganado y se agrego a tu corral. +msg.2630bd23d7d4=seguro para salto +msg.2648db667f58=lanza golpe +msg.26c2f4289162=Cargar partida. Selecciona personaje. Presiona Suprimir para borrar una partida. +msg.270dcc07d3ae=trueno medio +msg.271e9a568c0e=salto +msg.2727056eb878=Corral construida. Ganado {arg1} de {arg2} +msg.273d131f0c8e=Sin notificaciones. +msg.27a582f91d6b={arg1} son invadiendo desde {arg2}! +msg.27bbfb11a294=Repite el magico patron a earn favor desde el dioses. +msg.27e4cdae51e3=vampiro4 +msg.27f1dd42841b=- Presiona Enter para confirmar +msg.27f5254de319=Almacen no construido +msg.27f76f96867d={arg1}: +{arg2}. +msg.282b619d84bc=- O lucha contra el Unicornio directamente (tiene mucha salud) +msg.285df50e183e=- Presiona SPACE para lanzar tu lanza cuando el murcielago suene centrado +msg.29a2d9e8aadb=Trampa holds un {arg1}. +msg.29a828259ef4=Fabricaste {arg1} Piel Sombreros. +msg.29d1fab76b47=Fabricaste un junco basket. +msg.29f291640516=Instrucciones de mision +msg.2a72e1170f55=No horses fueron capturado. +msg.2afb87ae1752=- Corres automaticamente, la velocidad aumenta con el tiempo +msg.2b436c9ddbbb=Descuartizaste pato. Obtuviste 1 carne, plumas y plumon. +msg.2b8ff62c083e=Debes estar en la base para usar el altar. +msg.2badd283a63b=Ya has desbloqueado el Fylgja de {arg1}. +msg.2bb11d7c39dc=Un enemigo esta al alcance de tu arma equipada actualmente. +msg.2bf7b1d59f34=unicornio cae +msg.2c6c80931cd1=Piel Sombreros +msg.2cd0d08a829a=Fabricaste {arg1} Incienso. +msg.2db4abf99659=Nueva partida iniciada. +msg.2e094dd0a2ab=lanza golpe +msg.2efa88864653=El bumeran te golpeo.{arg1} +msg.3030e8ec7633=Pescado +msg.30680e90d434=Ahumado {arg1} pescado en {arg2} pescado ahumado. +msg.311f421748f9=Necesitas un carcaj para fabricar flechas. +msg.31342e72b48c={arg1} se va. +msg.32b43edbf3b0=El almacen debe mejorarse antes de un corral. +msg.32c8e74e0055=Fabricaste un Carcaj. +msg.33bc40d8201e=- Despues de la melodia, elige la cantidad de notas +msg.33c75a645734=Fabricaste un cuerda de arco. +msg.33c89b4bdc4e=carcaj +msg.34daac1528e0=menu mover +msg.34e4ec0621b4=El almacen debe mejorarse antes de un establo. +msg.34fcedc354d9=pesca caña +msg.363b1d2abd57=Comida en almacen {arg1} carne, {arg2} pescado ahumado, {arg3} cestas de frutas y nueces. Total {arg4}. Consumo diario {arg5} +msg.364118a099cd=Victoria del Unicornio +msg.36e876dbcc17=Tienes una conexion {arg1} con {arg2}. +msg.37423f3bd00f=pieles +msg.37bb70fed953=Trampa es puesta y vacia. +msg.37ef0562902c=liana +msg.381aea2617cb=Incienso +msg.38a7045d078c=trueno bajo +msg.38eb0fe3f78a=flecha flies +msg.39268e59f7ba=Trampa es puesta but no activa yet. +msg.3976f81bbfef=Tu {arg1} es leaving. +msg.39784ab5ac68=piece de tendon +msg.39a531d92bbe=flecha +msg.3a7d9767b123=objeto +msg.3ba80d6f0cfb=Runas. {arg1} +msg.3bc5191d2dc2=Iniciado escalando arbol. Height es {arg1} pies. +msg.3c32451c9c71=piel bolsas +msg.3ce35dadcc67=Establo es lleno. No horses fueron capturado. +msg.3cebb4c50866=cana de pescar +msg.3dd6d1aafd1c=Fabricaste {arg1} Piel Guantes. +msg.3e024b87e650=duro piedra +msg.3e03229d39e4=Pieles +msg.3e2cee02d8c1=El cielo begins a aclararse. +msg.3e575fbcccad=No buildings disponible. +msg.3e919b422247=El fuego en x {arg1} y {arg2} se ha apagado. +msg.3eb9cd52ec8d=piel guantes +msg.3f098cc2c738=Pausado. Presiona retroceso para reanudar. +msg.3f35ee808cb2=Fabricaste {arg1} Piel Tunicas. +msg.3f638a51cb47=E o U - Cuarta nota (tono mas alto) +msg.3fab86f11579=Este arbol es vacia. +msg.3ffc8c72cadf=Un {arg1} escapo mientras un residente revisaba la trampa en x {arg2}. +msg.4010ceb056dd=trueno alto +msg.403654dac805=hueso4 +msg.4037ea7ad431=lanzas +msg.408a612a4167=espectro1 +msg.409251fa0d30=El altar debe construirse en la base. +msg.412fed81f58a=junco basket +msg.417705583821=viento medio +msg.41a682cadfe3=volar +msg.423b7f343fb8=Pescado en el linea. +msg.4248c4c1f5a9=Incienso ardiendo. {arg1} horas restante. +msg.425dbf90058e=Fabricaste {arg1} Hondas. +msg.4272004d80b4=jabali canal +msg.427944aa680d=Piel Bolsas +msg.42b2620d1afa=- Los lanzamientos precisos dan 2 puntos cada uno +msg.42da7d0b84a8=- Cada salto exitoso da 2 puntos +msg.434d81f4ecc4=Fallo a abrir archivo: {arg1} +msg.43696d3d5fb8=Grabado {arg1}. +msg.437ecef821db=lanza falla +msg.43821cae64c5=cuerdas de arco +msg.44328bf27ea3=Arrojaste {arg1} palos en el fuego. +msg.444d4678962b=caida +msg.4525b0152629=No equipo disponible a grabar. +msg.4537c16badd9=arco fuego +msg.455b11a16eb7=trampas +msg.45a604519c0d=El bandidos cut tu plumon. +msg.45bcf18418f1=Necesitas estar a 2 casillas o menos de un arroyo. +msg.46fcdd91c99e=paso draugrhaugr +msg.470f9d20c2b3=Fabricaste {arg1} Cuerda. +msg.474c797713f3=troncos +msg.4798100ccc16=Skeletal Bard completo. Tu puntuacion para all five tunes es {arg1}. +msg.483a17dd2a17=Trampas +msg.484adfaab5cd=Fabricaste {arg1} Arcos. +msg.494f87e3874d=draugrhaugr 1 +msg.495f723e9fa1=hachas +msg.4a0e932d1bc5=grava +msg.4a17d4fabe93=piel tunicas +msg.4b9b4385b1a9=bumeran +msg.4bce12c3add1=No estas en la base. +msg.4bd5f5ba3c71=Retiraste {arg1}. +msg.4c0d2469f78b=piedras +msg.4c8578f10e74=Ya hay una fogata en la base. +msg.4d723065565f=Tu heave un tronco en el fuego. +msg.4db797458510=Fabricaste {arg1} Arcilla Ollas. +msg.4e5ee9d0128e=Sin mascota. +msg.4ea30f60b92f=El grabado de runas solo puede hacerse en la zona de base. +msg.4ecf016b2d3f=Ya hay una fogata aqui. +msg.4f08301dcb7b=notificacion +msg.4f11c6797b8d=poco profunda agua +msg.4f4dcdd4262d=Establo construido. +msg.4f773e9d5bf0=Arcos +msg.4f7f358cd341=ganso +msg.4f99d3d5b638=Arcilla Ollas +msg.4ff23d0c3700=Fallaste el bumeran.{arg1} +msg.5009c8e190ee=pavo +msg.508e8bc0cc57=- Usa Arriba/Abajo para cambiar el numero +msg.5123c22ce6a5=honda bajo alcance +msg.514b1402c9f3=Safely alcanzaste el suelo. +msg.515f45b3270f=Alcanzaste el cima en {arg1} pies. +msg.52140591224d=El dioses son pleased con tu victoria! {arg1} favor otorgado. +msg.528fb1c4e5fb=Nuevo juego +msg.529f0f749181=No {arg1} disponible. +msg.52f1fd1916c9=Quemar incienso +msg.5397e0583f14=Si +msg.53da80cba4ff=Collected {arg1} y trampa. +msg.5460f2e3c43e=aplastar +msg.546330ee64db=cana de pescar romper +msg.546f2ff04577=Trampa puesta. +msg.54fc2d657ed6=Draugnorak +msg.55748cf43121=vampiro3 +msg.55c9d2b3e8a4=Fabricaste un Piel Tunica. +msg.5665ef30ef4d=Los murcielagos invaden! Lanza tu lanza para defenderte. +msg.57b0465e575a=Se ha descubierto una cordillera al este! +msg.57e2db156a64=No trampa nearby. +msg.57e457c02ba4=Alimentar fuego con tronco +msg.584227d57290=Juncos +msg.5906872f9bb6=vampiro2 +msg.596350ae8387=Encontraste un junco. +msg.599096c44c5b=Jabali Canales +msg.5993f9da16fc=Aprender sonidos. +msg.59b74cafdedb=Murcielago invasion completo. Puntuacion {arg1}. +msg.59eab77f00cc=UN {arg1} es quedando hungry. +msg.5a7c581c0427=Fabricaste {arg1} Trampas. +msg.5b61e4982b59=espectro muere +msg.5bdb7fa2223d=Plumas +msg.5be2899acbe8=El proposito de los residentes se desvanece. +msg.5c2434ecd90f=juncos +msg.5cac31311668=El corral esta lleno. No se recupero ganado. +msg.5cc7429297c8=ese puedes ser destroyed con un hacha. +msg.5cda1b25dd32=UN {arg1} es hungry y unresponsive. +msg.5d1fa9e5a5ef=Un residente ha sido capturado. +msg.5de6d49b4e89=El area se ha expandido! Nuevo territorio descubierto al este. +msg.5dfc869b7588=lanzamiento fuerza +msg.5e09b31082c5=No trampas a colocar. +msg.5ea84b1201bb={arg1} favor grants tu el eyes de un aguila. +msg.5f3f3a85393b=menu seleccionar +msg.600007c39e3f=No aventuras disponible en el base. +msg.612e052d92d2=Barricada es ya en lleno de salud. +msg.61b1ce299103={arg1}. {arg2} restante. +msg.623f78278373=Barricada salud {arg1} de {arg2} +msg.62583aa87a59=Corral o almacen no construido. No se recupero ganado. +msg.627a5b9e6322=trepar arbol +msg.62b1e8703ede=Fabricaste un Lanza. +msg.62fac2f438bb=halcon +msg.64875fcccaac=pescado +msg.64b945d9072e=Huerto de hierbas no construido +msg.656dac78b857=Atrapaste {arg1} bumeranes para un total de {arg2} puntos. +msg.65eb0fb6ab40=piel pantalones +msg.666d7125f17b=No hay misiones disponibles. +msg.66fd9a7e91f8=lianas +msg.67ae077e6458=Huerto de hierbas construido +msg.686021c25738=hueso3 +msg.6861fd95f4f7=- Hay 5 turnos +msg.6a26c7cfd66b=Depositado {arg1}. +msg.6a33218f6325=Tu {arg1} el bumeran. {arg2} puntos.{arg3} +msg.6c265b98505c=hacha golpe +msg.6cbfe49b4adf=No se pudo cargar el sonido. +msg.6cf4935744c0=Canoas +msg.6d18e60e7952=base destruida +msg.6d4a0243dbfd=Archivo guardada successfully +msg.6d9c2a31fbbc=arcilla ollas +msg.6e247a17f21a=Presiona Enter cuando estes listo para empezar, o Escape para cancelar. +msg.6e32618ee300=cuchillo +msg.6fdc01a81c89=Ahora puedes grabar equipo con esta runa en el menu de fabricacion. +msg.704e634dd69f=golpe suelo +msg.708019666e3c=Fabricaste {arg1} Carcajes. +msg.70a5f57b592d=No hay altar construido. +msg.714165bec103=colocar trampa +msg.717d7fcc68b7=- Hay 5 rondas, hasta 4 puntos cada una +msg.71aad5f523eb=piel sombreros +msg.731152a028c6=Cerrado. +msg.757117781964=Los residentes recolectaron {arg1} cestas de frutas y nueces. +msg.7680f62917b1=- Presiona Space otra vez cuando suene cerca +msg.76c1490aad8c=No incienso a quemar. +msg.77346d0447da=plumon +msg.7748b68d141a=profundo bosque +msg.778a4ff05e50=Descuartizaste pavo. Obtuviste 1 carne y plumas. +msg.7822e0af08c8=Altar ya construido. +msg.78f036ea9ebd=El beast charges back y forth relentlessly. +msg.7921110eb7e1=fabricacion +msg.7968afd0c044=pesca cañas +msg.7a61d6b17282=Necesitas una olla de arcilla para quemar incienso. +msg.7b50bfc0ef4b=Fabricaste {arg1} cestas de junco. +msg.7babc233de26=tronco +msg.7c49f713a0fb=flechas +msg.7c57f16628c7=Encontraste arcilla. +msg.7cc940c217a7=Escalando {arg1}. {arg2} pies. +msg.7d2cf35450e3=Escucha al melody y cantidad el notas. +msg.7d88f361f3e8=zombi1 +msg.7e83de91b4a2=bandido hembra muere +msg.7f6990bc78a8=Fabricaste {arg1} Piedra Hachas. +msg.7f7e3bbf8864=Un enemigo ya no esta al alcance de tu arma equipada actualmente. +msg.7fae743693f4=Mochilas +msg.802aa6576577=Cargar partida +msg.803e4c41bbd2=palos +msg.80655da8d80a=arbol +msg.806baaf8e039=canoas +msg.816c52fd2bdd=No +msg.81df04a81e24={arg1} favor shines upon tu. {arg2} +msg.823be948275f=- Un mejor tiempo da mas puntos (hasta 4) +msg.8257ec29507d=Arrojaste {arg1} troncos en el fuego. +msg.837affd1b6ba=Reforzaste la barricada {arg1} veces con piedras. Salud ahora {arg2}. +msg.83865f248682=No muertos mas poderosos. No te pierden el rastro cuando estas en la base. +msg.83a5c4369598=Fabricaste un Piedra Cuchillo. +msg.83fa126f6e3f={arg1} puesta a ranura {arg2}. +msg.8420d3438dd8=Menu de Fylgja. +msg.8443de859917=viento alto +msg.849a4f4a399d=Tu son huyendo desde el realm del muertos! +msg.84c5ff34e186=Fabricaste {arg1} Piedra Cuchillos. +msg.8551c25cde92=El incienso se ha consumido. +msg.8569a552d825=x {arg1}, terreno {arg2}. Base {arg3} casillas este. Barricada {arg4} de {arg5}. +msg.85a177a79f1d=hueso2 +msg.85f15bc57d6c=pluma +msg.862021235188=canoa +msg.862614facb79=Sin comida, los residentes tienen hambre. +msg.8626f8c56627=Fabricaste Piel Guantes. +msg.86290fe7e711=hueso5 +msg.86e257c3efcb=Notificacion mas antigua. {arg1} +msg.87383ce4344d=Cuerdas de arco +msg.87cdb5c91437=cuerdas +msg.87ee697acb01=Altar construido. +msg.8818f1d55166={arg1} de radiancia llena de proposito a los residentes. +msg.885cda3d707c=Tu huir el hideout. +msg.88749dbbbf09==== Murcielago Invasion === +msg.88a8bee5206e=- Atrae al Unicornio al puente +msg.8aab96177c7d=Revisar trampa +msg.8b9881d57a1a=Esta zona ya no tiene nada. +msg.8c3fa20e31d0=Tu matched {arg1} notas. Puntuacion {arg2}. +msg.8d0ad3cf8648=Partida deleted. +msg.8d6c04ec96c7=El pescado es still en el linea. +msg.8e19643ce0e6={arg1} desequipado. +msg.8ee83610f314=mocasines +msg.8f1379e43b0f=Piel Pantalones +msg.8f69276b9332=Lanza un bumeran y atrapalo en el regreso. +msg.8f993ac20023=Entras en un paso de montana estrecho. Un Unicornio enorme bloquea tu camino! +msg.8feaa2e73a19=Info de base. {arg1} +msg.9007bd6e52db=No enough palos. +msg.90c991ce4eec=Altar. Favor: {arg1}. +msg.917ee46db0cd=abeja +msg.922b0aa0ef1a=Reforzaste la barricada {arg1} veces con lianas. Salud ahora {arg2}. +msg.924e0eb6b6f1=jugador masculino danio +msg.9281d2fe5f08={arg1} favor shines upon tu. Tu feel swift para un mientras. +msg.92849349ef1f=UN horse se une tu establo. +msg.92abc6d6953f=trepar cuerda +msg.92b702e15649=Has desbloqueado el Fylgja de {arg1}! +msg.93682970720f=Fabricaste incienso. +msg.9379874d592b=Altar no construido. No favor otorgado. +msg.93852d36202b=Fabricaste {arg1} Mocasines. +msg.93894d088f6a=- Usa tu hacha para destruir un soporte del puente +msg.93fa0af97c4f=Ya hay un huerto de hierbas en la base. +msg.93fc8e4d1331=Grabado {arg1} {arg2} con {arg3}. +msg.946466f6c963=palo +msg.94c221dc0888=Modo de practica. Presiona Enter para empezar, Escape para cancelar. +msg.957f554c342a=jabalI +msg.9612a65234f9={arg1} atrapado en trampa en x {arg2} y 0! +msg.96901dc80a5b=No materiales a reforzar el barricada. +msg.96b0a63888d1=No puede colocar trampas en el base area. +msg.97c59037a63c=Un superviviente se une a tu base. +msg.97dcc91b2ee2=Pisaste tu trampa y se rompio! +msg.988157738a23=No aventuras encontrado en este area. +msg.98ae12aa6a63=Dejas de pescar. +msg.995d4e06ae4e=pescado ahumado +msg.998b61086fa7=Recogiste un objeto para el cual no hay un sonido especifico. +msg.9afdb7e61391=- Presiona Space para lanzar +msg.9b64062b2074=Escondite de Bandidos +msg.9b81947e1398=Los residentes mataron a un {arg1} atacante. +msg.9bbcfc6c8f9d=bandido4 +msg.9c19f78d931d=hueso6 +msg.9dd40321998e=murcielago2 +msg.9ea4f66d0740=Revisar fuego +msg.9fcdb8881bce=Tu huir el encounter. +msg.a040b931b9bb=Como a jugar: +msg.a0844149a18c=Introdujiste {arg1} notas y el numero real fue {arg2}. {arg3} puntos. Presiona Enter para continuar. +msg.a153dba5e67a=profundo agua +msg.a2620cbc10f5=dia +msg.a294ccba07b9=Fogata construido here. +msg.a2cc4be07e5e=Menu de aventura. +msg.a2d7f5d60d98=caza menor +msg.a2f3a6c42a8f=miscelaneo +msg.a3347af174f3=Corral no construida +msg.a3cb738850fa=lobo +msg.a3f2439ef01a=piel sombrero +msg.a56be51786dc=hueso1 +msg.a570b024087d=Siempre hambrientos, se alimentan de tus residentes. +msg.a641f39a6317=- La carrera termina cuando caes en una tumba +msg.a646f0dcb735=Favor otorgado: {arg1}. +msg.a6b2cd34521f=El sol es poniendose. +msg.a6c587a6a19d=Un pez esta en el anzuelo! +msg.a6caf0c3771d=hondas +msg.a6e2492f34b6==== Escape desde Hel === +msg.a75c9ab9b807=Establo ya construido. +msg.a79dc18e77d9=- Tienes 10 intentos +msg.a7cb7754af6c=Notificacion mas reciente. {arg1} +msg.a95eab2b45da=- Salta (flecha ARRIBA) para evitar ser atropellado +msg.a9cf87f2edd7=Fabricaste {arg1} Piel Pantalones. +msg.aa3a1916166d=duende1 +msg.aa5edb7494f3=arcilla olla +msg.ab2244bc6027=pergamino de curacion +msg.abd02188f443=Un residente recupero {arg1} de la trampa en x {arg2} y 0 y la reinicio. +msg.abe73973cc7c=No se pudo cargar la partida. +msg.ac36af678d78=Controles: +msg.ac66e08b54d7=Reanudado. +msg.ac8ccb330272=UN {arg1} se une tu. +msg.ad34f00c46cd=mocasin +msg.ad36d64ad3a3=duende muere +msg.ad610e155539=El velocidad blessing se desvanece. +msg.ade25255fbc1=Fabricaste un Canoa. +msg.ae534dc25ad3=Los residentes repararon la barricada. +{arg1} de salud. +msg.af1bd00fc4be=Nada a sacrificar. +msg.af8df8d37140=Fabricaste {arg1} Canoas. +msg.af93482d73f0=pergaminos de curacion +msg.aff5100bebc5=negro gato +msg.b0b80b668953=Fabricaste {arg1} Lanzas. +msg.b0cca9f9205a=Palos +msg.b0f6aa37f763=Lianas +msg.b11aa393c8a8=Mocasines +msg.b23d59c4bbce=Fabricaste un Honda. +msg.b31837f2fc5b=iniciar pesca +msg.b32fc272dc0d=- Escucha murcielagos pasar de izquierda a derecha +msg.b38e4cff23de=Un residente atrapo un pescado y lo agrego al almacen. +msg.b46513dbc687=Retiraste {arg1} {arg2}. +msg.b48d0661f0da=Piel Tunicas +msg.b4a2314b1d80=Este arbol ya fue talado. +msg.b4a9d19a4e85=Selecciona numero de notas. {arg1}. +msg.b5d5cc084a95=bandido3 +msg.b5f1924def3d=Un residente ahumo un pescado y obtuvo {arg1} pescado ahumado. +msg.b66ee1230dac=Reforzaste la barricada con piedras. +{arg1} de salud. Ahora {arg2} de {arg3}. +msg.b73a0709297f=llamar mascota +msg.b774102c89b5=Cuchillos +msg.b8de1d1a935f=Los residentes mataron a un bandido atacante. +msg.b94e9f3d7e00=nieve +msg.ba3af68702a3=Learned Rune de Destruction! +msg.ba7d7dfe330b=Cestas de frutas y nueces +msg.bad2fcec0edf=Fabricaste mocasines. +msg.bb0ea68db3b4=cesta de frutas y nueces +msg.bb40f75a9c60=carne +msg.bbc29b76c59b=hierba +msg.bbfd1384acaf=Fabricaste un arcilla olla. +msg.bd4796595f1f=No hay palos para alimentar el fuego. +msg.bd573ed65ffe=No pescado en el linea. +msg.bdebda25dad9=flecha golpe +msg.bf57e001639c=Fabricaste un Piel Bolsa. +msg.bf93e5ce8bc1=Plumon +msg.bfc7e2fc2280=Reforzaste la barricada con tronco. +{arg1} de salud. Ahora {arg2} de {arg3}. +msg.c01409b3a728=Al lanzar, suelta Control cuando estes sobre el agua. Al atrapar, suelta cuando el sonido este sobre el jugador. +msg.c05feaca7269=No objetos fueron recuperado. +msg.c1104d82c0bd=trampa +msg.c2ff4bc695b7=Objeto no disponible. +msg.c3080306943b=Menu de equipo. +msg.c3567877be0e=Fabricaste {arg1} flechas. +msg.c418fde85a0d=Fabricaste un Piel Sombrero. +msg.c477acc3f80b=Solo puedes llevar {arg1} flechas con tus carcajes actuales. +msg.c48d642737b2=Habita regiones montanosas. +msg.c4f7e9c21601=Ahumado Pescado +msg.c50641e24962=Tu lanzamiento fallo el agua y cayo en la vegetacion cercana, donde es poco probable atraer peces. +msg.c50d661da7cf=La barricada ha caido! +msg.c52888225c69=caida +msg.c5565f654184=Hierba huerto construido. El base ahora heals faster. +msg.c6186586864e=x {arg1}, terreno {arg2}. Unicornio facing {arg3} +msg.c65e5dfc5ba7=bandido1 +msg.c72b7844929d=Bandidos defeated: {arg1}. +msg.c82e3d7279ef=arroyo +msg.c86ace9b97f8=Establo construido. Horses {arg1} de {arg2} +msg.c8d882f9098c=Los residentes atraparon {arg1} pescados y los agregaron al almacen. +msg.c8da1c4f1714=Baja de tono a medida que avanza la caida. +msg.c8fee26f6ee3=Rechazado. +msg.c90399f41b3f=Carne +msg.c9532ab465fb=hueso7 +msg.cb6c212139a1=Learned Rune de Swiftness! +msg.cb8b9677952a=Almacen recompensas: +msg.cb8e70ab2bcc=jabali canales +msg.cc167fb645e4=cuerda de arco +msg.cc3caa9812e2=No hay troncos para alimentar el fuego. +msg.cc61d0f3de65=cestas de junco +msg.ccb9a6240155=Necesitas una fogata a 2 casillas o menos para encender un fuego. +msg.cccc5c842adb=hacha golpe +msg.ccce38999a27=Corral ya construido. +msg.cd13e6ce9297=El eagle's sight se desvanece. +msg.cd3ef7da3f4f=Depositado {arg1} {arg2}. +msg.cd4c1cc68057=Quemando! {arg1} de salud restantes. +msg.ce13e4341cdc=entrar alcance +msg.ce3897c6b3be={arg1}: {arg2} +msg.ce5aeca95fb9=objeto breaks +msg.cea88b214070={arg1} {arg2} +msg.cfe2650b4c6e=Encontraste una piedra. +msg.d04a59a6a71b=Controles: IZQUIERDA/DERECHA para moverte, ARRIBA para saltar, CTRL para atacar, ESC para huir +msg.d0cb2acd0739=piel +msg.d103663bcfd9=Sacrificado 1 {arg1}. Favor +{arg2}. Total {arg3}. +msg.d1c5baa8a36b=Reforzaste la barricada con palos. +{arg1} de salud. Ahora {arg2} de {arg3}. +msg.d34b6217ea75=enemigo cae +msg.d373510877fd=Piel Guantes +msg.d39008c4a392=Excepcion no controlada +msg.d49f2e760778=mochila +msg.d537941485fb=R o I - Tercera nota +msg.d5dd6ddda4ef=Fuego construido en fogata. +msg.d5e86e1918e8=Nada para equipar. +msg.d5e89cddb940=El establo debe construirse en la base. +msg.d609f615ddae=No se encontraron partidas guardadas. +msg.d63eba28afc0=oeste +msg.d6dc139f529a=invasion enemiga +msg.d6fd3467a950=yeti 1 +msg.d75f36f88797=Hachas +msg.d7879bd506af=No se recupero ganado. +msg.d8ea835664e1=Tu base esta en capacidad maxima. +msg.da0b5a461d98=F o K - Primera nota (tono mas bajo) +msg.da11dc5e090c=honda golpe +msg.da93fb9992a3=vampiro muere +msg.dadcebf86f43=Caught un {arg1} {arg2}. +msg.dadd68431e60=Habita regiones nevadas. +msg.db2a8514a0fc=salir alcance +msg.db476a28fe5b=Menu de misiones. +msg.db56e2e1db9f=Aventura +msg.db5bbabf1bc3=bandido2 +msg.dbea5ffbac7b=Draugnorak. Menu principal. +msg.dc5c17f80fbe=No puedes hacer eso. +msg.ddea499514d8=Ya usaste tu Fylgja hoy. +msg.ddf360815408=Tendon +msg.ddfc9c735c48=Bandidos will, de course, no take este lying plumon. +msg.de0dce8ef11c=cestas de frutas y nueces +msg.de2c51fece28=Altar no construido +msg.de4e8495f978=paso +msg.de6b29cb1808=arcilla +msg.de752f6fdd8b=plumas +msg.df3f34f3f144=UN skeleton named Billy Bones es practicing a become un bard. +msg.df63c8846d76=Objetivo: +msg.df93ea08d751=Fabricaste {arg1} Piel Bolsas. +msg.dfa22c1cada0=Fabricaste {arg1} Mochilas. +msg.dffaa4b11d31=Sacrificado {arg1} {arg2}. Favor +{arg3}. Total {arg4}. +msg.e00d5664a78e=Arcilla +msg.e0b678c82a60=Un puente de madera cruza un abismo profundo adelante. El puente tiene dos soportes +msg.e0c7f029cfa2=No runas desbloqueado yet. +msg.e142524d7689=Estrategia: +msg.e1c16d0490ee=No hay sonidos disponibles. +msg.e1dc1099a799=Fabricaste {arg1} Pesca Cañas. +msg.e253d297aabc=piel tunica +msg.e2e91797da96=Pesca Cañas +msg.e30bfd0c38dc=piedra +msg.e31811cdce94=Fabricaste cuerda. +msg.e34161b98188=malo lanzamiento +msg.e379741bfbd3=Pergaminos de curacion: +{arg1}. +msg.e3ce3e543239=Carcajes +msg.e3d828480491=honda alto alcance +msg.e4427f09bc09=Cierra esta pantalla para comenzar. +msg.e5a51ddbb159=Alimentar fuego con liana +msg.e5d79aba2048=no muerto residente1 +msg.e5f656d506a7=zombi golpe +msg.e6526216c064=Has saqueado el tesoro, y los Draugr estan furiosos! +msg.e65fa8960222=Ya has dominado la Runa de Destruccion. +msg.e6ae58426f13=hueso8 +msg.e7409fcb54bc=alimentar fuego +msg.e74f453392b6=Collected trampa. +msg.e75d3068a5e9=Reforzaste la barricada con lianas. +{arg1} de salud. Ahora {arg2} de {arg3}. +msg.e8002c169040=madera +msg.e8216720d30b=- Presiona SPACE para saltar sobre tumbas +msg.e8db3a42f86f=Aprender sonidos +msg.e8e8f4ee7a5a=Ahora puedes grabar armas con esta runa en el menu de fabricacion. +msg.e8fe153261a9=No enough troncos. +msg.e9dd1d268b65=La base esta lejos al este, protegida por una barricada. +msg.ea6bfdb7bd29=El Unicornio trampled tu. +msg.ea771032e597=Altar construido +msg.ead6f13efc8d=Necesitas una cana de pescar equipada. +msg.eb9cac43e997=Fabricaste un Trampa. +msg.ebf229b227f8={arg1} {arg2} de {arg3} +msg.ec465c0c4000=Necesitas terreno abierto frente a ti para cargar. +msg.eced46d198ed=Un residente recolecto una cesta de frutas y nueces. +msg.ed989350becb=Alcanzaste elevation {arg1}. +msg.edbcc0a199d1={arg1} de salud. +msg.ee02d6d4c199=Tu stepped en tu trampa! El {arg1} escapo. +msg.ee200b3d8ae2=Lanzas +msg.eff5a5f34d9a=unicornio en puente +msg.f015f25c409b=arco draw +msg.f08ee9fe3340=Comenzando. Repite el patron. +msg.f12b2d219fe8=Cuerdas +msg.f137213e72e2=Se ha visto un enjambre de zombis. +msg.f16169ef79aa=Not quite a zombi nor a vampyr. These no muerto remember they used para have a home, and "\n "maybe there's food there... +msg.f1c671ae3045=Base info. No opciones. +msg.f229791ba917=Caiste {arg1} pies! Took {arg2} damage. {arg3} de salud restantes. +msg.f305da2c3550==== Atrapa el Bumeran === +msg.f32ea7a06f91={arg1}, x {arg2}, y {arg3}, terreno {arg4} +msg.f3c5ed579a54=viento bajo +msg.f3d964225e83=jugador hembra danio +msg.f3e51f53f970=Corral construido. +msg.f40ad52f4d97=Reforzaste la barricada {arg1} veces con tronco. Salud ahora {arg2}. +msg.f48b940f83c7=Descuartizaste {arg1}. Obtuviste 1 carne y 1 piel. +msg.f50492d2703a=Descuartizaste ganso. Obtuviste 1 carne, plumas y plumon. +msg.f5cabf873590=junco +msg.f5f6c13f2ceb=Un residente agrego {arg1} al almacen. +msg.f77fc018cb7b=Encuentras una base de bandidos oculta en lo profundo del bosque. +msg.f83b6fe3aebf=Salir +msg.f85b4b604c9b={arg1}. +msg.f8909d26fe86=no muerto residente muere +msg.f8b2a45e7e4b=Fabricaste un Arco. +msg.f8fc573e2d2f=Cargar partida (sin partidas guardadas) +msg.fa5bd38090c7=arco +msg.fa87b71ab25a=Partida cargada. +msg.fa88a4c8033c=Ya hay un fuego en la base. +msg.fb091149bb52=Fabricaste Piel Pantalones. +msg.fb09c6d50b11=UN nuevo mision es disponible: {arg1}. +msg.fb389469701e=Almacen mejorado. La capacidad ahora es {arg1} por objeto. +msg.fb4a022a82b3=unicornio galopando +msg.fbec17cb2fcb=lluvia +msg.fc1740dbb4ae=Sin coincidencias para {arg1}. +msg.fc1ad6e10ac4={arg1} ({arg2} disponibles) +msg.fc5294f9b150=Fabricaste {arg1} Cuerdas de arco. +msg.fca1af3d07b0=Pergaminos de curacion +msg.fd1a25749457=Volumen {arg1}. +msg.fdd0b57cb7b9=Tu toss un fiew lianas y leaves en el fuego. +msg.fe0ede7d0360=Almacen es fully mejorado. +msg.fe2a9d6606e0=draugnorak +msg.fe406c6e0055=Comenzando. +msg.feea4a3b0a84==== Unicornio Hunt === +msg.fefc084f67de=D o J - Segunda nota +msg.ff5ac0b6df7b=arcos +msg.ffed2db61296=Ahumado un pescado en {arg1} pescado ahumado. diff --git a/libstorm-nvgt b/libstorm-nvgt index 7531eac..59f2880 160000 --- a/libstorm-nvgt +++ b/libstorm-nvgt @@ -1 +1 @@ -Subproject commit 7531eacf64591946fbeb41da442e4cbf647e3d7a +Subproject commit 59f288049826e86c33f597e9db77c094a2435ecb diff --git a/minigame.nvgt b/minigame.nvgt new file mode 100644 index 0000000..19d994c --- /dev/null +++ b/minigame.nvgt @@ -0,0 +1,77 @@ +#include "include/bgt_compat.nvgt" +#include "include/sound_pool.nvgt" + +sound_pool p(30); + +#include "src/constants.nvgt" +#include "src/speech_history.nvgt" +#include "src/text_reader.nvgt" + +#include "src/quests/bat_invasion_game.nvgt" +#include "src/quests/catch_the_boomerang_game.nvgt" +#include "src/quests/enchanted_melody_game.nvgt" +#include "src/quests/escape_from_hel_game.nvgt" +#include "src/quests/skeletal_bard_game.nvgt" + +void quest_menu_background_tick() { + // No world state to tick in the minigame test menu. +} + +void quest_boomerang_hit_sound() { + p.play_stationary("sounds/player_male_damage.ogg", false); +} + +void run_minigame_menu() { + speak_with_history("Minigames menu.", true); + + int selection = 0; + string[] options = {"Bat Invasion", "Catch the Boomerang", "Enchanted Melody", + "Escape from Hel", "Skeletal Bard", "Exit"}; + speak_with_history(options[selection], true); + + while (true) { + wait(5); + if (key_pressed(KEY_ESCAPE)) { + exit(); + } + + 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)) { + if (selection == 0) + run_bat_invasion(); + else if (selection == 1) + run_catch_the_boomerang(); + else if (selection == 2) + run_enchanted_melody(); + else if (selection == 3) + run_escape_from_hel(); + else if (selection == 4) + run_skeletal_bard(); + else + exit(); + show_window("Minigames"); + speak_with_history("Minigames menu.", true); + speak_with_history(options[selection], true); + } + } +} + +void main() { + p.volume_step = AUDIO_VOLUME_STEP / float(AUDIO_TILE_SCALE); + p.pan_step = AUDIO_PAN_STEP; + show_window("Minigames"); + run_minigame_menu(); +} diff --git a/package.sh b/package.sh new file mode 100755 index 0000000..48da1ee --- /dev/null +++ b/package.sh @@ -0,0 +1,223 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# create Draugnorak package for all platforms. + +tmpAndroidToolsDir="" +tmpAndroidConfigBackup="" +createdAndroidConfig="0" +tmpAndroidWrapperSource="" +tmpAndroidPackFile="" +tmpAndroidPackBuilder="" + +trap 'rm -fv README.html; if [[ -n "${tmpAndroidToolsDir}" && -d "${tmpAndroidToolsDir}" ]]; then rm -rf "${tmpAndroidToolsDir}"; fi; if [[ -n "${tmpAndroidConfigBackup}" && -f "${tmpAndroidConfigBackup}" ]]; then mv -f "${tmpAndroidConfigBackup}" draugnorak.properties; elif [[ "${createdAndroidConfig}" == "1" ]]; then rm -f draugnorak.properties; fi; if [[ -n "${tmpAndroidWrapperSource}" && -f "${tmpAndroidWrapperSource}" ]]; then rm -f "${tmpAndroidWrapperSource}"; fi; if [[ -n "${tmpAndroidPackBuilder}" && -f "${tmpAndroidPackBuilder}" ]]; then rm -f "${tmpAndroidPackBuilder}"; fi; if [[ -n "${tmpAndroidPackFile}" && -f "${tmpAndroidPackFile}" ]]; then rm -f "${tmpAndroidPackFile}"; fi' EXIT + +escape_nvgt_string() { + local inputValue="$1" + inputValue="${inputValue//\\/\\\\}" + inputValue="${inputValue//\"/\\\"}" + printf '%s\n' "${inputValue}" +} + +find_android_sdk_root() { + local sdkRoot + for sdkRoot in "${ANDROID_HOME:-}" "${ANDROID_SDK_HOME:-}" "/opt/android-sdk" "${HOME}/Android/Sdk" "${HOME}/Android/sdk"; do + if [[ -n "${sdkRoot}" && -d "${sdkRoot}" ]]; then + printf '%s\n' "${sdkRoot}" + return 0 + fi + done + return 1 +} + +find_latest_build_tools_dir() { + local sdkRoot="$1" + local buildToolsDir="" + if [[ ! -d "${sdkRoot}/build-tools" ]]; then + return 1 + fi + buildToolsDir="$(find "${sdkRoot}/build-tools" -mindepth 1 -maxdepth 1 -type d | sort -V | tail -n 1)" + if [[ -z "${buildToolsDir}" ]]; then + return 1 + fi + printf '%s\n' "${buildToolsDir}" +} + +find_latest_android_jar() { + local sdkRoot="$1" + local androidJar="" + if [[ ! -d "${sdkRoot}/platforms" ]]; then + return 1 + fi + androidJar="$(find "${sdkRoot}/platforms" -mindepth 2 -maxdepth 2 -type f -name android.jar | sort -V | tail -n 1)" + if [[ -z "${androidJar}" ]]; then + return 1 + fi + printf '%s\n' "${androidJar}" +} + +setup_android_tools_workaround() { + local sdkRoot="$1" + local buildToolsDir="$2" + local androidJar="$3" + local javaBin="" + local keytoolBin="" + local adbBin="" + + tmpAndroidToolsDir="$(mktemp -d)" + + ln -sf "${buildToolsDir}/zipalign" "${tmpAndroidToolsDir}/zipalign" + ln -sf "${buildToolsDir}/lib/apksigner.jar" "${tmpAndroidToolsDir}/apksigner.jar" + ln -sf "${androidJar}" "${tmpAndroidToolsDir}/android.jar" + + if command -v java >/dev/null 2>&1; then + javaBin="$(command -v java)" + ln -sf "${javaBin}" "${tmpAndroidToolsDir}/java" + fi + + if command -v keytool >/dev/null 2>&1; then + keytoolBin="$(command -v keytool)" + ln -sf "${keytoolBin}" "${tmpAndroidToolsDir}/keytool" + fi + + if command -v adb >/dev/null 2>&1; then + adbBin="$(command -v adb)" + ln -sf "${adbBin}" "${tmpAndroidToolsDir}/adb" + fi + + cat > "${tmpAndroidToolsDir}/aapt2" < "${builderScriptPath}" <> "${builderScriptPath}" + done < <(find sounds -type f | sort) + + cat >> "${builderScriptPath}" <<'EOF' + soundPack.close(); +} +EOF + + ./nvgt "${builderScriptPath}" + if [[ ! -f "${outputPackPath}" ]]; then + echo "Failed to build Android sound pack: ${outputPackPath}" + exit 1 + fi +} + +# Generate documentation +pandoc -s files/instructions.md -o README.html + +# Create packages +for platformName in linux mac windows android; do + echo "Packaging Draugnorak for ${platformName^}..." + if [[ "${platformName}" == "android" ]]; then + androidKeystorePath="${DRAUGNORAK_ANDROID_KEYSTORE:-${PWD}/.nvgt_android.keystore}" + sdkRoot="$(find_android_sdk_root || true)" + if [[ -z "${sdkRoot}" ]]; then + echo "Android SDK not found. Set ANDROID_HOME or ANDROID_SDK_HOME." + exit 1 + fi + buildToolsDir="$(find_latest_build_tools_dir "${sdkRoot}" || true)" + if [[ -z "${buildToolsDir}" || ! -x "${buildToolsDir}/aapt2" || ! -x "${buildToolsDir}/zipalign" || ! -f "${buildToolsDir}/lib/apksigner.jar" ]]; then + echo "Missing required Android build tools in ${sdkRoot}." + exit 1 + fi + androidJar="$(find_latest_android_jar "${sdkRoot}" || true)" + if [[ -z "${androidJar}" ]]; then + echo "No android.jar found in ${sdkRoot}/platforms." + exit 1 + fi + setup_android_tools_workaround "${sdkRoot}" "${buildToolsDir}" "${androidJar}" + + if [[ -f draugnorak.properties ]]; then + tmpAndroidConfigBackup="$(mktemp)" + cp draugnorak.properties "${tmpAndroidConfigBackup}" + cat "${tmpAndroidConfigBackup}" > draugnorak.properties + else + createdAndroidConfig="1" + fi + { + printf '%s\n' "build.android_install = 0" + printf '%s\n' "build.android_signature_cert = ${androidKeystorePath}" + printf '%s\n' "build.output_basename = draugnorak" + } >> draugnorak.properties + + tmpAndroidPackFile=".draugnorak_android_sounds.dat" + tmpAndroidPackBuilder=".draugnorak_make_android_pack.nvgt" + build_android_sound_pack "${tmpAndroidPackFile}" "${tmpAndroidPackBuilder}" + + tmpAndroidWrapperSource=".draugnorak_android_bundle_pragmas.nvgt" + cat > "${tmpAndroidWrapperSource}" <<'EOF' +#pragma embed ".draugnorak_android_sounds.dat" +#pragma asset files +#pragma document "README.html" + +pack@ androidSoundPack; +bool preglobals() { + @androidSoundPack = pack(); + if (androidSoundPack.open("*.draugnorak_android_sounds.dat", PACK_OPEN_MODE_READ)) { + @sound_default_pack = @androidSoundPack; + } + return true; +} +EOF + + PATH="${tmpAndroidToolsDir}:${PATH}" ./nvgt -c --platform "${platformName}" --include "${tmpAndroidWrapperSource}" draugnorak.nvgt + else + ./nvgt -c --platform "${platformName}" draugnorak.nvgt + fi + case "${platformName}" in + "linux"|"windows") + zip -r draugnorak.zip files/ sounds/ README.html + mv draugnorak.zip "draugnorak-${platformName}.zip" + ;; + "mac") + mkdir -p draugnorak.app/Contents/Resources + cp -r files draugnorak.app/Contents/Resources + cp -r sounds draugnorak.app/Contents/Resources + cp README.html draugnorak.app/Contents/Resources + zip -r draugnorak.app.zip draugnorak.app fix_mac.sh + rm -rfv draugnorak.app/ + ;; + "android") + zip -r draugnorak-android.zip draugnorak.apk files/ sounds/ README.html + ;; + esac +done + +echo "Packaging complete!" + +exit 0 diff --git a/scripts/audit_untranslated_strings.py b/scripts/audit_untranslated_strings.py new file mode 100755 index 0000000..e3c9f99 --- /dev/null +++ b/scripts/audit_untranslated_strings.py @@ -0,0 +1,401 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import re +import sys +from pathlib import Path +from typing import Dict, Iterable, List, Optional, Set, Tuple + +ROOT = Path(__file__).resolve().parents[1] +ALLOWLIST_PATH = ROOT / "scripts" / "i18n_audit_allowlist.txt" + +SKIP_DIR_NAMES = {".git", "bloodshed", "docs", "skills", "nvgt-git", "libstorm-nvgt"} +SKIP_FILE_NAMES = {"crash.log"} + +INSERT_LAST_CONTEXT_HINTS = ( + "option", + "label", + "line", + "prompt", + "instruction", + "intro", + "reward", + "message", + "title", + "menu", +) + +TRANSLATION_WRAPPERS = ( + "tr(", + "trf(", + "trn(", + "i18n_translate_speech_message(", + "i18n_lookup_key_with_fallback(", + "speech_history_transform_message(", + "get_barricade_option_text(", + "i18n_text(", +) + +# Function call checks for call arguments that must be translation-wrapped when they +# contain literals. Keep this conservative and focused on user-facing text paths. +ARG_CHECKS: Dict[str, List[int]] = { + "screen_reader_speak": [0], + "menu_run_simple": [0], + "text_reader": [0, 1], + "text_reader_lines": [1], + "text_reader_file": [1], + "file_viewer": [0, 1], + "file_viewer_lines": [1], + "file_viewer_file": [1], +} + +ASSIGNMENT_LHS_CHECKS = ( + "intro_text", +) + + +class Finding: + def __init__(self, path: Path, line: int, context: str, expression: str): + self.path = path + self.line = line + self.context = context + self.expression = expression.strip() + + def key(self) -> str: + return f"{self.path.relative_to(ROOT).as_posix()}:{self.line}:{self.context}" + + +def iter_nvgt_files() -> List[Path]: + files: List[Path] = [] + + entrypoints = [ROOT / "draugnorak.nvgt", ROOT / "excluded_sounds.nvgt"] + for entry in entrypoints: + if entry.exists(): + files.append(entry) + + source_roots = [ROOT / "src", ROOT / "libstorm-nvgt"] + for source_root in source_roots: + if not source_root.exists(): + continue + for path in source_root.rglob("*.nvgt"): + rel = path.relative_to(ROOT) + if any(part in SKIP_DIR_NAMES for part in rel.parts): + continue + if path.name in SKIP_FILE_NAMES: + continue + files.append(path) + + return sorted(set(files)) + + +def load_allowlist() -> Set[str]: + allowed: Set[str] = set() + if not ALLOWLIST_PATH.exists(): + return allowed + + for raw_line in ALLOWLIST_PATH.read_text(encoding="utf-8", errors="replace").splitlines(): + line = raw_line.strip() + if not line or line.startswith("#"): + continue + allowed.add(line) + return allowed + + +def is_identifier_char(ch: str) -> bool: + return ch.isalnum() or ch == "_" + + +def read_identifier_backward(text: str, before_index: int) -> str: + i = before_index + while i >= 0 and text[i].isspace(): + i -= 1 + end = i + while i >= 0 and is_identifier_char(text[i]): + i -= 1 + start = i + 1 + if end < start: + return "" + return text[start : end + 1] + + +def find_matching_paren(text: str, open_index: int) -> int: + depth = 0 + in_string = False + escape = False + + for i in range(open_index, len(text)): + ch = text[i] + if in_string: + if escape: + escape = False + elif ch == "\\": + escape = True + elif ch == '"': + in_string = False + continue + + if ch == '"': + in_string = True + continue + if ch == "(": + depth += 1 + continue + if ch == ")": + depth -= 1 + if depth == 0: + return i + continue + + return -1 + + +def split_top_level(expr: str, delimiter: str) -> List[str]: + parts: List[str] = [] + depth_paren = 0 + depth_bracket = 0 + depth_brace = 0 + in_string = False + escape = False + start = 0 + + for i, ch in enumerate(expr): + if in_string: + if escape: + escape = False + elif ch == "\\": + escape = True + elif ch == '"': + in_string = False + continue + + if ch == '"': + in_string = True + continue + if ch == "(": + depth_paren += 1 + continue + if ch == ")": + depth_paren = max(0, depth_paren - 1) + continue + if ch == "[": + depth_bracket += 1 + continue + if ch == "]": + depth_bracket = max(0, depth_bracket - 1) + continue + if ch == "{": + depth_brace += 1 + continue + if ch == "}": + depth_brace = max(0, depth_brace - 1) + continue + + if ch == delimiter and depth_paren == 0 and depth_bracket == 0 and depth_brace == 0: + parts.append(expr[start:i]) + start = i + 1 + + parts.append(expr[start:]) + return parts + + +def line_number_for_index(text: str, index: int) -> int: + return text.count("\n", 0, index) + 1 + + +def has_string_literal(expr: str) -> bool: + return len(extract_string_literals(expr)) > 0 + + +def extract_string_literals(expr: str) -> List[str]: + literals: List[str] = [] + in_string = False + escape = False + current: List[str] = [] + + for ch in expr: + if in_string: + if escape: + current.append(ch) + escape = False + continue + if ch == "\\": + escape = True + continue + if ch == '"': + in_string = False + literals.append("".join(current)) + current = [] + continue + current.append(ch) + continue + + if ch == '"': + in_string = True + + return literals + + +def has_meaningful_literal(expr: str) -> bool: + literals = extract_string_literals(expr) + if not literals: + return False + + for literal in literals: + if literal == "": + continue + if re.fullmatch(r"[\s:,.!?;()\-\[\]/+]*", literal): + continue + if re.fullmatch(r"[a-z0-9_.-]+", literal): + # Translation keys and identifiers are not user-facing copy. + continue + return True + + return False + + +def is_translated_expression(expr: str) -> bool: + normalized = "".join(expr.split()) + for wrapper in TRANSLATION_WRAPPERS: + if wrapper in normalized: + return True + return False + + +def should_check_insert_last(receiver: str) -> bool: + receiver_lower = receiver.lower() + if not receiver_lower: + return False + return any(hint in receiver_lower for hint in INSERT_LAST_CONTEXT_HINTS) + + +def add_finding(findings: List[Finding], path: Path, line: int, context: str, expr: str) -> None: + findings.append(Finding(path, line, context, expr)) + + +def check_call_args(path: Path, line: int, function_name: str, receiver: str, args: List[str], findings: List[Finding], + translated_arrays: Set[str]) -> None: + if function_name == "insert_last": + if not should_check_insert_last(receiver): + return + if receiver in translated_arrays: + return + if not args: + return + expr = args[0] + if has_meaningful_literal(expr) and not is_translated_expression(expr): + add_finding(findings, path, line, f"{receiver}.insert_last", expr) + return + + target_indexes = ARG_CHECKS.get(function_name) + if not target_indexes: + return + + for arg_index in target_indexes: + if arg_index >= len(args): + continue + expr = args[arg_index] + if has_meaningful_literal(expr) and not is_translated_expression(expr): + add_finding(findings, path, line, f"{function_name}[{arg_index}]", expr) + + +def check_assignment_literals(path: Path, text: str, findings: List[Finding]) -> None: + for lhs in ASSIGNMENT_LHS_CHECKS: + pattern = re.compile(rf"\b{re.escape(lhs)}\s*=\s*(.+?);", re.MULTILINE) + for match in pattern.finditer(text): + expr = match.group(1) + if not has_meaningful_literal(expr): + continue + if is_translated_expression(expr): + continue + line = line_number_for_index(text, match.start()) + add_finding(findings, path, line, f"assign:{lhs}", expr) + + +def scan_file(path: Path) -> List[Finding]: + findings: List[Finding] = [] + text = path.read_text(encoding="utf-8", errors="replace") + translated_arrays = set(re.findall(r"i18n_translate_string_array_in_place\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)", text)) + + check_assignment_literals(path, text, findings) + + i = 0 + while i < len(text): + ch = text[i] + if not is_identifier_char(ch): + i += 1 + continue + + start = i + while i < len(text) and is_identifier_char(text[i]): + i += 1 + name = text[start:i] + + j = i + while j < len(text) and text[j].isspace(): + j += 1 + + if j >= len(text) or text[j] != "(": + continue + + receiver = "" + k = start - 1 + while k >= 0 and text[k].isspace(): + k -= 1 + if k >= 0 and text[k] == ".": + receiver = read_identifier_backward(text, k - 1) + + close = find_matching_paren(text, j) + if close < 0: + break + + arg_text = text[j + 1 : close] + args = split_top_level(arg_text, ",") + + tail = close + 1 + while tail < len(text) and text[tail].isspace(): + tail += 1 + if tail < len(text) and text[tail] == "{": + # Function/method declaration, not a call site. + i = close + 1 + continue + + line = line_number_for_index(text, start) + check_call_args(path, line, name, receiver, args, findings, translated_arrays) + + i = close + 1 + + return findings + + +def summarize_expression(expr: str) -> str: + collapsed = " ".join(expr.split()) + if len(collapsed) > 120: + return collapsed[:117] + "..." + return collapsed + + +def main() -> int: + allowlist = load_allowlist() + + all_findings: List[Finding] = [] + for nvgt_file in iter_nvgt_files(): + all_findings.extend(scan_file(nvgt_file)) + + filtered = [f for f in all_findings if f.key() not in allowlist] + filtered.sort(key=lambda item: (item.path.as_posix(), item.line, item.context)) + + if not filtered: + print("No untranslated-string violations found.") + return 0 + + print(f"Found {len(filtered)} untranslated-string violations:") + for finding in filtered: + rel = finding.path.relative_to(ROOT).as_posix() + print(f"{rel}:{finding.line}: {finding.context}: {summarize_expression(finding.expression)}") + + print("\nAdd approved exceptions to scripts/i18n_audit_allowlist.txt if needed.") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/generate_i18n_catalog.py b/scripts/generate_i18n_catalog.py new file mode 100644 index 0000000..dd23352 --- /dev/null +++ b/scripts/generate_i18n_catalog.py @@ -0,0 +1,660 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import hashlib +import re +from pathlib import Path +from typing import Dict, List, Optional, Tuple + +ROOT = Path(__file__).resolve().parents[1] + +TARGET_FUNCTION_ARGS = { + "speak_with_history": [0], + "screen_reader_speak": [0], + "notify": [0], + "learn_sounds_speak": [0], + "learn_sounds_add_description": [1], + "notifications_speak": [0], + "ui_info_box": [0, 1, 2], + "ui_question": [0, 1], + "ui_input_box": [0, 1], + "virtual_info_box": [0, 1, 2], + "virtual_question": [0, 1], + "virtual_input_box": [0, 1], + "text_reader": [1], + "text_reader_lines": [1], + "text_reader_file": [1], +} + +INSERT_LAST_CONTEXT_HINTS = ( + "option", + "label", + "line", + "prompt", + "instruction", + "intro", + "reward", + "message", + "title", + "menu", +) + +PASS_THROUGH_TEXT_FUNCTIONS = { + "i18n_text", + "i18n_translate_speech_message", +} + +SKIP_DIR_NAMES = {".git", "bloodshed", "docs", "skills", "nvgt-git"} +SKIP_FILE_NAMES = {"crash.log"} + + +def iter_nvgt_files() -> List[Path]: + files: List[Path] = [] + + entrypoints = [ROOT / "draugnorak.nvgt", ROOT / "excluded_sounds.nvgt"] + for entry in entrypoints: + if entry.exists(): + files.append(entry) + + source_roots = [ROOT / "src", ROOT / "libstorm-nvgt"] + for source_root in source_roots: + if not source_root.exists(): + continue + for path in source_root.rglob("*.nvgt"): + rel = path.relative_to(ROOT) + if any(part in SKIP_DIR_NAMES for part in rel.parts): + continue + if path.name in SKIP_FILE_NAMES: + continue + files.append(path) + + return sorted(set(files)) + + +def is_identifier_char(ch: str) -> bool: + return ch.isalnum() or ch == "_" + + +def read_identifier_backward(text: str, before_index: int) -> str: + i = before_index + while i >= 0 and text[i].isspace(): + i -= 1 + end = i + while i >= 0 and is_identifier_char(text[i]): + i -= 1 + start = i + 1 + if end < start: + return "" + return text[start : end + 1] + + +def find_matching_paren(text: str, open_index: int) -> int: + depth = 0 + in_string = False + escape = False + + for i in range(open_index, len(text)): + ch = text[i] + if in_string: + if escape: + escape = False + elif ch == "\\": + escape = True + elif ch == '"': + in_string = False + continue + + if ch == '"': + in_string = True + continue + if ch == "(": + depth += 1 + continue + if ch == ")": + depth -= 1 + if depth == 0: + return i + continue + + return -1 + + +def split_top_level(expr: str, delimiter: str) -> List[str]: + parts: List[str] = [] + depth_paren = 0 + depth_bracket = 0 + depth_brace = 0 + in_string = False + escape = False + start = 0 + + for i, ch in enumerate(expr): + if in_string: + if escape: + escape = False + elif ch == "\\": + escape = True + elif ch == '"': + in_string = False + continue + + if ch == '"': + in_string = True + continue + if ch == "(": + depth_paren += 1 + continue + if ch == ")": + depth_paren = max(0, depth_paren - 1) + continue + if ch == "[": + depth_bracket += 1 + continue + if ch == "]": + depth_bracket = max(0, depth_bracket - 1) + continue + if ch == "{": + depth_brace += 1 + continue + if ch == "}": + depth_brace = max(0, depth_brace - 1) + continue + + if ch == delimiter and depth_paren == 0 and depth_bracket == 0 and depth_brace == 0: + parts.append(expr[start:i]) + start = i + 1 + + parts.append(expr[start:]) + return parts + + +def unescape_string_literal(literal_body: str) -> str: + result: List[str] = [] + i = 0 + while i < len(literal_body): + ch = literal_body[i] + if ch != "\\": + result.append(ch) + i += 1 + continue + + if i + 1 >= len(literal_body): + result.append("\\") + break + + nxt = literal_body[i + 1] + if nxt == "n": + result.append("\n") + elif nxt == "r": + result.append("\r") + elif nxt == "t": + result.append("\t") + else: + result.append(nxt) + i += 2 + + return "".join(result) + + +def expression_to_template(expr: str) -> Optional[str]: + expr = expr.strip() + if not expr: + return None + + parts = split_top_level(expr, "+") + template_parts: List[str] = [] + placeholder_count = 0 + + for raw_part in parts: + part = raw_part.strip() + if not part: + continue + + passthrough_expr = unwrap_passthrough_expression(part) + if passthrough_expr is not None: + passthrough_template = expression_to_template(passthrough_expr) + if passthrough_template is not None: + template_parts.append(passthrough_template) + continue + + if len(part) >= 2 and part[0] == '"' and part[-1] == '"': + template_parts.append(unescape_string_literal(part[1:-1])) + else: + placeholder_count += 1 + template_parts.append(f"{{arg{placeholder_count}}}") + + if not template_parts: + return None + + template = "".join(template_parts) + if template.strip() == "": + return None + if template_literal_length(template) == 0: + return None + return template + + +def unwrap_passthrough_expression(expr: str) -> Optional[str]: + expr = expr.strip() + match = re.match(r"^([A-Za-z_][A-Za-z0-9_]*)\((.*)\)$", expr, re.DOTALL) + if not match: + return None + + function_name = match.group(1) + if function_name not in PASS_THROUGH_TEXT_FUNCTIONS: + return None + + arg_text = match.group(2) + args = split_top_level(arg_text, ",") + if not args: + return None + + return args[0].strip() + + +def template_literal_length(template_text: str) -> int: + literal_count = 0 + in_placeholder = False + + for ch in template_text: + if not in_placeholder and ch == "{": + in_placeholder = True + continue + if in_placeholder and ch == "}": + in_placeholder = False + continue + if not in_placeholder: + literal_count += 1 + + return literal_count + + +def line_number_for_index(text: str, index: int) -> int: + return text.count("\n", 0, index) + 1 + + +def add_entry(entries: Dict[str, Dict[str, object]], template: str, source_ref: str) -> None: + base_key = f"msg.{hashlib.sha1(template.encode('utf-8')).hexdigest()[:12]}" + key = base_key + suffix = 1 + while key in entries and entries[key]["value"] != template: + key = f"{base_key}_{suffix}" + suffix += 1 + + if key not in entries: + entries[key] = {"value": template, "refs": [source_ref]} + else: + refs: List[str] = entries[key]["refs"] # type: ignore[assignment] + if source_ref not in refs: + refs.append(source_ref) + + +def extract_from_call( + entries: Dict[str, Dict[str, object]], + receiver: str, + function_name: str, + args: List[str], + source_ref_base: str, +) -> None: + target_arg_indexes: List[int] = [] + + if function_name in TARGET_FUNCTION_ARGS: + target_arg_indexes = TARGET_FUNCTION_ARGS[function_name] + elif function_name == "insert_last": + receiver_lower = receiver.lower() + if not any(hint in receiver_lower for hint in INSERT_LAST_CONTEXT_HINTS): + return + target_arg_indexes = [0] + else: + return + + for arg_index in target_arg_indexes: + if arg_index >= len(args): + continue + template = expression_to_template(args[arg_index]) + if not template: + continue + source_ref = f"{source_ref_base}:{function_name}[{arg_index}]" + add_entry(entries, template, source_ref) + + +def scan_file(path: Path, entries: Dict[str, Dict[str, object]]) -> None: + text = path.read_text(encoding="utf-8", errors="replace") + i = 0 + + while i < len(text): + ch = text[i] + if not is_identifier_char(ch): + i += 1 + continue + + start = i + while i < len(text) and is_identifier_char(text[i]): + i += 1 + name = text[start:i] + + j = i + while j < len(text) and text[j].isspace(): + j += 1 + + if j >= len(text) or text[j] != "(": + continue + + receiver = "" + k = start - 1 + while k >= 0 and text[k].isspace(): + k -= 1 + if k >= 0 and text[k] == ".": + receiver = read_identifier_backward(text, k - 1) + + close = find_matching_paren(text, j) + if close < 0: + break + + arg_text = text[j + 1 : close] + args = split_top_level(arg_text, ",") + + line = line_number_for_index(text, start) + rel_path = path.relative_to(ROOT).as_posix() + source_ref_base = f"{rel_path}:{line}" + extract_from_call(entries, receiver, name, args, source_ref_base) + + i = close + 1 + + +def slugify_fragment(value: str) -> str: + slug = re.sub(r"[^a-z0-9]+", "_", value.lower()) + slug = slug.strip("_") + return slug or "value" + + +def add_item_registry_fragments(entries: Dict[str, Dict[str, object]]) -> None: + item_registry_path = ROOT / "src" / "item_registry.nvgt" + if not item_registry_path.exists(): + return + + text = item_registry_path.read_text(encoding="utf-8", errors="replace") + pattern = re.compile( + r"ItemDefinition\([^,]+,\s*\"([^\"]+)\",\s*\"([^\"]+)\",\s*\"([^\"]+)\"", + re.MULTILINE, + ) + + for match in pattern.finditer(text): + plural = match.group(1) + singular = match.group(2) + display = match.group(3) + + plural_slug = slugify_fragment(plural) + singular_slug = slugify_fragment(singular) + display_slug = slugify_fragment(display) + + add_entry(entries, plural, f"src/item_registry.nvgt:item_plural:{plural_slug}") + add_entry(entries, singular, f"src/item_registry.nvgt:item_singular:{singular_slug}") + add_entry(entries, display, f"src/item_registry.nvgt:item_display:{display_slug}") + + +def add_seed_messages(entries: Dict[str, Dict[str, object]]) -> None: + seeds = [ + "Load Game (no saves found)", + "No saves found.", + "Unable to load save.", + "Unhandled exception", + "Really exit?", + "Found a stone.", + "Found a reed.", + "Found clay.", + ] + for value in seeds: + add_entry(entries, value, "seed:manual") + + +def normalize_learn_sounds_label(sound_path: Path) -> str: + name = sound_path.stem.lower() + name = name.replace("_", " ").replace("-", " ") + name = re.sub(r"\s+", " ", name).strip() + return name or "sound" + + +def add_learn_sounds_label_seeds(entries: Dict[str, Dict[str, object]]) -> None: + sounds_dir = ROOT / "sounds" + if not sounds_dir.exists(): + return + + for path in sorted(sounds_dir.rglob("*")): + if not path.is_file(): + continue + if path.suffix.lower() not in {".ogg", ".wav"}: + continue + label = normalize_learn_sounds_label(path) + rel = path.relative_to(ROOT).as_posix() + add_entry(entries, label, f"seed:learn_sounds_label:{rel}") + + +def escape_for_ini(value: str) -> str: + escaped = value.replace("\\", "\\\\") + escaped = escaped.replace("\n", "\\n") + escaped = escaped.replace("\r", "\\r") + escaped = escaped.replace("\t", "\\t") + return escaped + + +def write_catalog(entries: Dict[str, Dict[str, object]], output_path: Path) -> None: + output_path.parent.mkdir(parents=True, exist_ok=True) + + manual_entries: List[Tuple[str, str]] = [ + ("meta.code", "en"), + ("meta.name", "English"), + ("meta.native_name", "English"), + ("system.language.select_prompt", "Select your language."), + ("system.language.selected", "Language set to {language}."), + ("system.language.english_label", "English (en)"), + ("system.ui.window_title", "Draugnorak"), + ("system.sex.male", "Male"), + ("system.sex.female", "Female"), + ("system.new_character.choose_sex", "Choose your sex."), + ("system.new_character.enter_name", "Enter your name or press Enter for random."), + ("system.new_character.save_exists_overwrite", "Save found for {name}. Overwrite?"), + ("system.load_game.option_with_metadata", "{name}, {sex}, day {day}"), + ("system.load_game.delete_confirm_base", "Are you sure you want to delete the character {name}?"), + ("system.load_game.delete_confirm_with_metadata", + "Are you sure you want to delete the character {name} gender {sex} days {day}?"), + ("system.load_game.delete_save_heading", "Delete Save"), + ("system.load_game.delete_save_failed", "Unable to delete save."), + ("system.quick_slot.no_item_bound", "No item bound to slot {slot}."), + ("system.menu.closed", "Closed."), + ("system.menu.canceled", "Canceled."), + ("system.menu.no_options", "No options."), + ("system.menu.no_matches", "No matches for {arg1}."), + ("system.option.no", "No"), + ("system.option.yes", "Yes"), + ("system.inventory.cant_carry_any_more_item", "You can't carry any more {item}."), + ("system.inventory.menu.prompt", "Inventory menu. {option}"), + ("system.inventory.menu.no_options", "Inventory menu. No options."), + ("system.inventory.option.personal", "Personal inventory"), + ("system.inventory.option.base_storage", "Base storage"), + ("system.inventory.count_set_to_slot", "{name} count set to slot {slot}."), + ("system.inventory.need_quiver_for_arrows", "You need a quiver to carry arrows."), + ("system.search.found_item", "Found {item}."), + ("system.search.found_nothing", "Found nothing."), + ("system.pickup.item", "Picked up {item}."), + ("system.storage.window_title", "Inventory"), + ("system.storage.transfer_prompt", "{prompt} (max {max})"), + ("system.storage.deposit_how_many", "Deposit how many?"), + ("system.storage.withdraw_how_many", "Withdraw how many?"), + ("system.storage.nothing_to_deposit", "Nothing to deposit."), + ("system.storage.nothing_to_withdraw", "Nothing to withdraw."), + ("system.storage.runed_cannot_deposit", "Runed items cannot be deposited into storage."), + ("system.storage.item_full", "Storage for that item is full."), + ("system.storage.no_storage_built", "No storage built."), + ("system.storage.menu_title", "Base storage."), + ("system.storage.menu.prompt", "Base storage. {option}"), + ("system.storage.menu.no_options", "Base storage. No options."), + ("system.crafting.require.fire_within_three_clay_pot", + "You need a fire within 3 tiles to craft a clay pot."), + ("system.crafting.require.fire_within_three_clay_pots", + "You need a fire within 3 tiles to craft clay pots."), + ("system.crafting.require.fire_within_three_bowstring", + "You need a fire within 3 tiles to make bowstring."), + ("system.crafting.require.altar_incense", "You need an altar to craft incense."), + ("system.crafting.require.fire_within_three_smoke_fish", + "You need a fire within 3 tiles to smoke fish."), + ("system.crafting.require.fire_within_three_butcher", "You need a fire within 3 tiles to butcher."), + ("system.character.slot.head", "head"), + ("system.character.slot.torso", "torso"), + ("system.character.slot.arms", "arms"), + ("system.character.slot.hands", "hands"), + ("system.character.slot.legs", "legs"), + ("system.character.slot.feet", "feet"), + ("system.character.menu.prompt", "Character info. {option}"), + ("system.character.menu.no_options", "Character info. No options."), + ("system.character.pet.no_pet", "No pet."), + ("system.character.pet.abandon_confirm", "Really abandon your pet? {option}"), + ("system.character.pet.unconscious_cannot_abandon", + "Your {pet} is unconscious. You can't abandon it right now."), + ("system.item.label.items", "items"), + ("system.item.label.item", "item"), + ("system.item.label.unknown", "Unknown"), + ("system.equipment.menu.equipped_suffix", " (equipped)"), + ("system.equipment.name.none", "None"), + ("system.equipment.name.unknown", "Unknown"), + ("system.equipment.name.spear", "Spear"), + ("system.equipment.name.stone_axe", "Stone Axe"), + ("system.equipment.name.sling", "Sling"), + ("system.equipment.name.bow", "Bow"), + ("system.equipment.name.skin_hat", "Skin Hat"), + ("system.equipment.name.skin_gloves", "Skin Gloves"), + ("system.equipment.name.skin_pants", "Skin Pants"), + ("system.equipment.name.skin_tunic", "Skin Tunic"), + ("system.equipment.name.moccasins", "Moccasins"), + ("system.equipment.name.skin_pouch", "Skin Pouch"), + ("system.equipment.name.backpack", "Backpack"), + ("system.equipment.name.fishing_pole", "Fishing Pole"), + ("system.equipment.name_plural.spears", "Spears"), + ("system.equipment.name_plural.stone_axes", "Stone Axes"), + ("system.equipment.name_plural.slings", "Slings"), + ("system.equipment.name_plural.bows", "Bows"), + ("system.equipment.name_plural.skin_hats", "Skin Hats"), + ("system.equipment.name_plural.skin_gloves", "Skin Gloves"), + ("system.equipment.name_plural.skin_pants", "Skin Pants"), + ("system.equipment.name_plural.skin_tunics", "Skin Tunics"), + ("system.equipment.name_plural.moccasins", "Moccasins"), + ("system.equipment.name_plural.skin_pouches", "Skin Pouches"), + ("system.equipment.name_plural.backpacks", "Backpacks"), + ("system.equipment.name_plural.fishing_poles", "Fishing Poles"), + ("system.equipment.name_plural.items", "Items"), + ("system.environment.tree.fell", "Tree fell!"), + ("system.environment.tree.fell_with_loot", + "Tree fell! Got {sticks} {sticks_label}, {vines} {vines_label}, and {logs} {logs_label}."), + ("system.environment.tree.inventory_full", "Inventory full."), + ("system.crafting.menu.prompt", "Crafting menu. {option}"), + ("system.crafting.category.weapons", "Weapons"), + ("system.crafting.category.tools", "Tools"), + ("system.crafting.category.materials", "Materials"), + ("system.crafting.category.clothing", "Clothing"), + ("system.crafting.category.buildings", "Buildings"), + ("system.crafting.category.barricade", "Barricade"), + ("system.crafting.category.runes", "Runes"), + ("system.crafting.weapons.prompt", "Weapons. {option}"), + ("system.crafting.weapons.option.spear", "Spear (1 Stick, 1 Vine, 1 Stone) [Requires Knife]"), + ("system.crafting.weapons.option.sling", "Sling (1 Skin, 2 Vines)"), + ("system.crafting.weapons.option.bow", "Bow (1 Stick, 1 Bowstring)"), + ("system.crafting.tools.prompt", "Tools. {option}"), + ("system.crafting.tools.option.stone_knife", "Stone Knife (2 Stones)"), + ("system.crafting.tools.option.snare", "Snare (1 Stick, 2 Vines)"), + ("system.crafting.tools.option.stone_axe", "Stone Axe (1 Stick, 1 Vine, 2 Stones) [Requires Knife]"), + ("system.crafting.tools.option.fishing_pole", "Fishing Pole (1 Stick, 2 Vines)"), + ("system.crafting.tools.option.rope", "Rope (3 Vines)"), + ("system.crafting.tools.option.quiver", "Quiver (2 Skins, 2 Vines)"), + ("system.crafting.tools.option.canoe", "Canoe (4 Logs, 11 Sticks, 11 Vines, 6 Skins, 2 Rope, 6 Reeds)"), + ("system.crafting.tools.option.reed_basket", "Reed Basket (3 Reeds)"), + ("system.crafting.tools.option.clay_pot", "Clay Pot (3 Clay)"), + ("system.crafting.materials.prompt", "Materials. {option}"), + ("system.crafting.materials.option.butcher_game", "Butcher Game [Requires Game, Knife, Fire nearby]"), + ("system.crafting.materials.option.smoke_fish", "Smoke Fish (1 Fish, 1 Stick) [Requires Fire nearby]"), + ("system.crafting.materials.option.arrows", "Arrows (2 Sticks, 4 Feathers, 2 Stones) [Requires Quiver]"), + ("system.crafting.materials.option.bowstring", "Bowstring (3 Sinew) [Requires Fire nearby]"), + ("system.crafting.materials.option.incense", "Incense (6 Sticks, 2 Vines, 1 Reed) [Requires Altar]"), + ("system.crafting.clothing.prompt", "Clothing. {option}"), + ("system.crafting.clothing.option.skin_hat", "Skin Hat (1 Skin, 1 Vine)"), + ("system.crafting.clothing.option.skin_gloves", "Skin Gloves (1 Skin, 1 Vine)"), + ("system.crafting.clothing.option.skin_pants", "Skin Pants (6 Skins, 3 Vines)"), + ("system.crafting.clothing.option.skin_tunic", "Skin Tunic (4 Skins, 2 Vines)"), + ("system.crafting.clothing.option.moccasins", "Moccasins (2 Skins, 1 Vine)"), + ("system.crafting.clothing.option.skin_pouch", "Skin Pouch (2 Skins, 1 Vine)"), + ("system.crafting.clothing.option.backpack", "Backpack (11 Skins, 5 Vines, 4 Skin Pouches)"), + ("system.crafting.buildings.prompt", "Buildings. {option}"), + ("system.crafting.buildings.option.firepit", "Firepit (9 Stones)"), + ("system.crafting.buildings.option.fire", "Fire (2 Sticks, 1 Log) [Requires Firepit]"), + ("system.crafting.buildings.option.herb_garden", "Herb Garden (9 Stones, 3 Vines, 2 Logs) [Base Only]"), + ("system.crafting.buildings.option.storage_upgrade_1", + "Upgrade Storage (6 Logs, 9 Stones, 8 Vines) [Base Only, 50 each]"), + ("system.crafting.buildings.option.storage_upgrade_2", + "Upgrade Storage (12 Logs, 18 Stones, 16 Vines) [Base Only, 100 each]"), + ("system.crafting.buildings.option.pasture", "Pasture (8 Logs, 18 Ropes) [Base Only, Requires Storage Upgrade]"), + ("system.crafting.buildings.option.stable", + "Stable (10 Logs, 15 Stones, 10 Vines) [Base Only, Requires Storage Upgrade]"), + ("system.crafting.buildings.option.altar", "Altar (9 Stones, 3 Sticks) [Base Only]"), + ("system.crafting.barricade.prompt", "Barricade. {option}"), + ("system.crafting.barricade.option.reinforce_sticks", + "Reinforce with sticks ({cost} sticks, +{health} health)"), + ("system.crafting.barricade.option.reinforce_vines", "Reinforce with vines ({cost} vines, +{health} health)"), + ("system.crafting.barricade.option.reinforce_log", "Reinforce with log ({cost} log, +{health} health)"), + ("system.crafting.barricade.option.reinforce_stones", + "Reinforce with stones ({cost} stones, +{health} health)"), + ("system.crafting.missing", "Missing: {requirements}"), + ("system.crafting.requirement.stone_knife", "Stone Knife"), + ("system.crafting.requirement.game", "Game"), + ("system.crafting.requirement.favor", "favor"), + ("system.crafting.requirement.resources", "resources"), + ] + + lines: List[str] = [] + lines.append("; Draugnorak localization catalog") + lines.append("; Copy this file to lang/.ini and translate only the right-hand values.") + lines.append("; Keep keys unchanged. Use placeholders like {arg1}, {count}, {language} exactly as written.") + lines.append("") + + lines.append("[meta]") + for key, value in manual_entries[:3]: + lines.append(f"{key.split('.', 1)[1]}={escape_for_ini(value)}") + + lines.append("") + lines.append("[system]") + for key, value in manual_entries[3:]: + lines.append(f"{key.split('.', 1)[1]}={escape_for_ini(value)}") + + lines.append("") + lines.append("[messages]") + + for key in sorted(entries.keys()): + value = entries[key]["value"] + refs: List[str] = entries[key]["refs"] # type: ignore[assignment] + for ref in refs[:3]: + lines.append(f"; {ref}") + lines.append(f"{key}={escape_for_ini(str(value))}") + + output_path.write_text("\n".join(lines) + "\n", encoding="utf-8") + + +def main() -> None: + entries: Dict[str, Dict[str, object]] = {} + + for file_path in iter_nvgt_files(): + scan_file(file_path, entries) + + add_item_registry_fragments(entries) + add_seed_messages(entries) + add_learn_sounds_label_seeds(entries) + + en_path = ROOT / "lang" / "en.ini" + template_path = ROOT / "lang" / "en.template.ini" + + write_catalog(entries, en_path) + write_catalog(entries, template_path) + + print(f"Wrote {en_path.relative_to(ROOT)} with {len(entries)} generated entries") + print(f"Wrote {template_path.relative_to(ROOT)}") + + +if __name__ == "__main__": + main() diff --git a/scripts/i18n_audit_allowlist.txt b/scripts/i18n_audit_allowlist.txt new file mode 100644 index 0000000..28d0b89 --- /dev/null +++ b/scripts/i18n_audit_allowlist.txt @@ -0,0 +1,2 @@ +# One entry per line: :: +# Use this only for intentional non-translated user-facing literals. diff --git a/scripts/validate_i18n_catalog.py b/scripts/validate_i18n_catalog.py new file mode 100644 index 0000000..6128de2 --- /dev/null +++ b/scripts/validate_i18n_catalog.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import re +import sys +from pathlib import Path +from typing import Dict, List, Set, Tuple + +ROOT = Path(__file__).resolve().parents[1] +LANG_DIR = ROOT / "lang" +BASE_FILE = LANG_DIR / "en.template.ini" +PLACEHOLDER_PATTERN = re.compile(r"\{([a-zA-Z0-9_]+)\}") + + +def parse_ini(path: Path) -> Dict[str, str]: + section = "" + result: Dict[str, str] = {} + + for raw_line in path.read_text(encoding="utf-8", errors="replace").splitlines(): + line = raw_line.strip() + if not line: + continue + if line.startswith(";") or line.startswith("#"): + continue + if line.startswith("[") and line.endswith("]"): + section = line[1:-1].strip() + continue + + if "=" not in line: + continue + key, value = line.split("=", 1) + key = key.strip() + value = value.strip() + if not key: + continue + + full_key = key if not section or "." in key else f"{section}.{key}" + result[full_key] = unescape_ini_value(value) + + return result + + +def unescape_ini_value(value: str) -> str: + out: List[str] = [] + i = 0 + while i < len(value): + ch = value[i] + if ch != "\\": + out.append(ch) + i += 1 + continue + + if i + 1 >= len(value): + out.append("\\") + break + + nxt = value[i + 1] + if nxt == "n": + out.append("\n") + elif nxt == "r": + out.append("\r") + elif nxt == "t": + out.append("\t") + else: + out.append(nxt) + i += 2 + + return "".join(out) + + +def placeholders(value: str) -> Set[str]: + return set(PLACEHOLDER_PATTERN.findall(value)) + + +def validate_language(base: Dict[str, str], target_path: Path) -> Tuple[List[str], List[str], List[str]]: + target = parse_ini(target_path) + + missing = sorted(set(base.keys()) - set(target.keys())) + extra = sorted(set(target.keys()) - set(base.keys())) + placeholder_issues: List[str] = [] + + for key in sorted(set(base.keys()) & set(target.keys())): + base_placeholders = placeholders(base[key]) + target_placeholders = placeholders(target[key]) + if base_placeholders != target_placeholders: + placeholder_issues.append( + f"{key}: expected {sorted(base_placeholders)}, found {sorted(target_placeholders)}" + ) + + return missing, extra, placeholder_issues + + +def main() -> int: + warn_extra_only = "--warn-extra" in sys.argv[1:] + + if not BASE_FILE.exists(): + print(f"Missing base template: {BASE_FILE}") + return 2 + + base = parse_ini(BASE_FILE) + + language_files = sorted(path for path in LANG_DIR.glob("*.ini") if path.name not in {"en.ini", "en.template.ini"}) + if not language_files: + print("No translation files found (expected lang/.ini).") + return 0 + + failed = False + + for path in language_files: + missing, extra, placeholder_issues = validate_language(base, path) + if not missing and not extra and not placeholder_issues: + print(f"{path.name}: OK") + continue + + has_blocking_issues = bool(missing or placeholder_issues or (extra and not warn_extra_only)) + if has_blocking_issues: + failed = True + print(f"{path.name}: FAIL") + else: + print(f"{path.name}: WARN") + if missing: + print(f" Missing keys ({len(missing)}):") + for key in missing[:20]: + print(f" - {key}") + if len(missing) > 20: + print(f" ... and {len(missing) - 20} more") + if extra: + print(f" Extra keys ({len(extra)}):") + for key in extra[:20]: + print(f" - {key}") + if len(extra) > 20: + print(f" ... and {len(extra) - 20} more") + if warn_extra_only and not missing and not placeholder_issues: + print(" Note: extra keys are warnings and do not fail validation.") + if placeholder_issues: + print(f" Placeholder mismatches ({len(placeholder_issues)}):") + for issue in placeholder_issues[:20]: + print(f" - {issue}") + if len(placeholder_issues) > 20: + print(f" ... and {len(placeholder_issues) - 20} more") + + return 1 if failed else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/skills/nvgt-engine-dev/SKILL.md b/skills/nvgt-engine-dev/SKILL.md new file mode 100644 index 0000000..a98f2e6 --- /dev/null +++ b/skills/nvgt-engine-dev/SKILL.md @@ -0,0 +1,66 @@ +--- +name: nvgt-engine-dev +description: Build, debug, refactor, and document NVGT (.nvgt) projects using authoritative NVGT engine sources. Use when tasks involve NVGT API usage, include/module selection, behavior verification against engine internals, migration from BGT-style patterns, menu/audio/UI architecture, or extracting reusable NVGT utilities. +--- + +# NVGT Engine Dev + +Use this skill to resolve NVGT questions from primary sources and produce clean, modular, reusable NVGT code. + +## Core Rules +- Verify API behavior with NVGT primary sources, not memory. +- Prefer docs first, then include implementation, then C++ bindings when behavior is unclear. +- Keep code simple, separated by concern, and reusable across projects. +- Keep accessibility first: rely on screen-reader APIs and keyboard-safe flows. + +## Workflow +1. Locate the NVGT source tree. +Default path is `~/git/nvgt`. +If missing, ask for `--root` override before making API claims. +2. Read `references/repo-map.md`. +Use it to pick the right source tier quickly. +3. Run lookup before coding. +Use `scripts/nvgt_lookup.py` for symbol-level discovery across docs/includes/bindings. +For signature-focused evidence, run `scripts/nvgt_api_report.py`. +4. Verify uncertain behavior in C++ bindings. +Read files under `/src/` where `RegisterGlobalFunction`, `RegisterObjectMethod`, or related registration calls are made. +5. Implement with modular boundaries. +Split generic utilities from game-specific logic. +6. Compile-check. +For Draugnorak-style projects, run `./nvgt -c draugnorak.nvgt` after changes. + +## Lookup Command +```bash +python3 skills/nvgt-engine-dev/scripts/nvgt_lookup.py "symbol_or_phrase" +``` + +Use `--root /path/to/nvgt` only when NVGT is not at `~/git/nvgt`. + +## API Report Command +```bash +python3 skills/nvgt-engine-dev/scripts/nvgt_api_report.py "symbol_name" +``` + +Use this report when you need signature/binding evidence before answering or coding. + +Use `--section` to narrow: +- `docs` +- `include-docs` +- `builtin-docs` +- `plugin-docs` +- `release-includes` +- `bindings` +- `samples` + +## References +- Read `references/project-profile.md` first when repo-specific style rules are absent. +- Read `references/repo-map.md` first for where things live in NVGT. +- Read `references/api-verification-workflow.md` when API behavior/signatures are uncertain. +- Read `references/nvgt-coding-patterns.md` when designing modules, menus, notifications, file viewers, and reusable libraries. +- Read `references/common-playbooks.md` for common implementation patterns. +- Run through `references/regression-checklist.md` before finalizing non-trivial NVGT changes. + +## Delivery Style +- Give direct, actionable answers. +- Show file-level references for where behavior was verified. +- Prefer minimal wrappers around engine primitives before adding abstraction. diff --git a/skills/nvgt-engine-dev/agents/openai.yaml b/skills/nvgt-engine-dev/agents/openai.yaml new file mode 100644 index 0000000..c75aefc --- /dev/null +++ b/skills/nvgt-engine-dev/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "NVGT Engine Dev" + short_description: "Use NVGT docs/source for clean modular scripting." + default_prompt: "Use NVGT authoritative docs, includes, and engine source to verify API behavior, design clean module boundaries, and implement accessible .nvgt game code." diff --git a/skills/nvgt-engine-dev/references/api-verification-workflow.md b/skills/nvgt-engine-dev/references/api-verification-workflow.md new file mode 100644 index 0000000..7de130a --- /dev/null +++ b/skills/nvgt-engine-dev/references/api-verification-workflow.md @@ -0,0 +1,44 @@ +# API Verification Workflow + +Use this order whenever a user asks NVGT API behavior, signatures, or edge cases. + +## Step 1: Locate candidate APIs in docs + +1. Search `doc/src/references/` for the symbol. +2. If the symbol looks include-specific, search `doc/src/references/include/`. +3. If built-in, search `doc/src/references/builtin/`. +4. If plugin-related, search `doc/src/references/plugin/`. + +## Step 2: Confirm scripted helper behavior + +Search `release/include/` for wrappers and defaults. + +Why: +- Includes often add defaults, convenience behavior, and compatibility layers not visible in short reference pages. + +## Step 3: Confirm engine truth in C++ + +When still ambiguous, inspect `src/*.cpp` bindings: + +- Search registration calls: +`RegisterGlobalFunction`, `RegisterObjectMethod`, `RegisterObjectProperty`, `RegisterFuncdef`. +- Verify exact exposed signatures and overloads. +- Read nearby implementation for side effects and constraints. + +## Step 4: Produce answer/change with evidence + +- Cite the exact file(s) used for verification. +- Prefer concise behavior statements over speculation. +- Mark inferred behavior explicitly when not directly declared. + +## Quick Commands + +```bash +# Broad symbol search across NVGT docs/includes/source +python3 skills/nvgt-engine-dev/scripts/nvgt_lookup.py "screen_reader_speak" --root ./nvgt-git +``` + +```bash +# Direct C++ binding verification +rg -n "RegisterGlobalFunction|RegisterObjectMethod|screen_reader_speak" nvgt-git/src +``` diff --git a/skills/nvgt-engine-dev/references/common-playbooks.md b/skills/nvgt-engine-dev/references/common-playbooks.md new file mode 100644 index 0000000..747d8fb --- /dev/null +++ b/skills/nvgt-engine-dev/references/common-playbooks.md @@ -0,0 +1,43 @@ +# Common Playbooks + +Use these when implementing frequent NVGT tasks. + +## 1. Extract Reusable Module + +1. Identify stable API boundary. +2. Move generic code to library module. +3. Leave game-specific data flow in project wrapper. +4. Add minimal setup hooks (callbacks/path settings). +5. Compile-check immediately. + +## 2. Build Menu Subsystem + +1. Choose engine primitive (`menu`, `audio_form`, or custom loop). +2. Define navigation keys and wrap behavior explicitly. +3. Add menu sounds by configurable path. +4. Add optional tick callback for background updates. +5. Verify keyboard-only operation and escape paths. + +## 3. Build Notification Subsystem + +1. Implement queue and bounded history. +2. Add key navigation for older/newer/latest entries. +3. Keep playback delay configurable. +4. Route speech through project-selected speak callback. +5. Verify behavior with and without notification sound files. + +## 4. Build File/Docs Viewer + +1. Keep viewer/editor generic. +2. Add project wrapper for labels/path conventions. +3. Ensure read-only mode is truly non-destructive. +4. Add optional save path for edit mode only. +5. Verify open failure and save failure speech paths. + +## 5. Add Global Volume Controls + +1. Centralize min/max/step and current dB. +2. Add key handler (PageUp/PageDown by default). +3. Support callback to apply volume in project audio layer. +4. Announce volume as percent for accessibility. +5. Verify no-op behavior at bounds. diff --git a/skills/nvgt-engine-dev/references/nvgt-coding-patterns.md b/skills/nvgt-engine-dev/references/nvgt-coding-patterns.md new file mode 100644 index 0000000..621ce50 --- /dev/null +++ b/skills/nvgt-engine-dev/references/nvgt-coding-patterns.md @@ -0,0 +1,43 @@ +# NVGT Coding Patterns + +Use these patterns for clean, separated, reusable NVGT code. + +## 1. Module Boundaries + +- Keep game-specific orchestration separate from reusable helpers. +- Put reusable helpers into dedicated library modules (e.g., `libstorm-nvgt/`). +- Keep adapters thin: +Project modules should configure and call reusable modules, not duplicate internals. + +## 2. Naming and Simplicity + +- Prefer snake_case for functions/methods. +- Prefer camelCase for variables. +- Keep APIs narrow: small setup functions + one runtime function. +- Avoid hidden globals unless they model subsystem state intentionally. + +## 3. Loop Safety + +- In long-running menu/game loops, keep `wait(5);`. +- Use per-loop tick callbacks for background tasks (volume keys, timers, notifications). +- Keep menu navigation behavior explicit (wrap on/off, exit keys, select keys). + +## 4. Accessibility-First UI + +- Prefer screen reader output and NVGT UI primitives (`form`, `virtual_dialogs`, `menu`) over external speech daemons. +- Ensure keyboard navigation has no traps. +- Keep labels complete and unambiguous. + +## 5. Audio Conventions + +- Use deterministic sound path conventions: +`sounds/menu/...`, `sounds/notify...`. +- Prefer extension fallback helpers (`.ogg`, then `.wav`) when building reusable modules. +- Guard optional sounds with existence checks. + +## 6. Verify, Then Generalize + +- Before extracting a helper, verify behavior from: +docs -> include wrapper -> C++ binding. +- Extract only stable behavior shared by multiple projects. +- Leave highly game-loop-specific logic in the project. diff --git a/skills/nvgt-engine-dev/references/project-profile.md b/skills/nvgt-engine-dev/references/project-profile.md new file mode 100644 index 0000000..8380955 --- /dev/null +++ b/skills/nvgt-engine-dev/references/project-profile.md @@ -0,0 +1,41 @@ +# Project Profile + +Use this profile when project-specific guidance is missing. +If the repo has `AGENTS.md` or equivalent, that file overrides this profile. + +## Default Engineering Style + +- Prefer clean separation of concerns. +- Keep reusable logic in standalone modules. +- Keep adapters/wrappers thin and obvious. +- Favor simple, direct control flow over clever abstractions. + +## Naming Defaults + +- Functions/methods: `snake_case` +- Variables: `camelCase` +- Classes/types: `PascalCase` + +## Accessibility Defaults + +- Prefer NVGT accessibility APIs and native screen-reader paths. +- Avoid unnecessary visual-only coupling in interaction flows. +- Ensure keyboard navigation is complete and trap-free. + +## Audio/UI Defaults + +- Use stable folder conventions for project-owned assets. +- Keep menu and notification sounds configurable by path. +- Support `.ogg` first and allow `.wav` fallback where practical. + +## Refactor Rules + +- Extract only behavior used across multiple modules/projects. +- Do not extract heavily game-loop-specific orchestration. +- Preserve behavior first; optimize structure second. + +## Delivery Expectations + +- Provide concise decisions with concrete file references. +- Explicitly state assumptions when source evidence is incomplete. +- Prefer incremental, verifiable changes over large rewrites. diff --git a/skills/nvgt-engine-dev/references/regression-checklist.md b/skills/nvgt-engine-dev/references/regression-checklist.md new file mode 100644 index 0000000..6a13b38 --- /dev/null +++ b/skills/nvgt-engine-dev/references/regression-checklist.md @@ -0,0 +1,35 @@ +# NVGT Regression Checklist + +Run this checklist after meaningful NVGT changes. + +## API/Source Verification +- Verified changed API assumptions against NVGT primary sources. +- For uncertain behavior, confirmed bindings in `/src/`. + +## Compile +- Project compile command succeeds. +- No new compile warnings/errors introduced. + +## Input/Navigation +- Keyboard navigation works end-to-end. +- Escape/close paths work from each UI/menu path. +- No keyboard traps. + +## Screen Reader +- New/changed controls announce meaningful labels. +- Important events are announced consistently. +- History/notification key paths still function if present. + +## Audio +- Required sound paths resolve correctly. +- Missing optional sounds fail gracefully. +- Long-running loops do not leak/dangle sound handles. + +## Data Safety +- File read/write failure paths are handled. +- Save/load or serialization paths preserve prior behavior. +- No accidental destructive behavior in migrations. + +## Reuse Boundary +- Generic vs project-specific code separation is clear. +- Extracted modules have minimal coupling. diff --git a/skills/nvgt-engine-dev/references/repo-map.md b/skills/nvgt-engine-dev/references/repo-map.md new file mode 100644 index 0000000..9cdb87b --- /dev/null +++ b/skills/nvgt-engine-dev/references/repo-map.md @@ -0,0 +1,73 @@ +# NVGT Repo Map + +Use this map to choose the fastest authoritative source in `nvgt-git`. + +## 1. Documentation First + +- `doc/src/manual/` +Practical tutorials, project workflows, and conceptual guidance. +Start here for process questions (build, compile/distribute, debugging scripts, concurrency/subscripting tutorials). + +- `doc/src/references/` +API entry points. +Use this when you need object/function names and category-level API navigation. + +- `doc/src/references/builtin/` +Built-in script API categories: +`Audio`, `Filesystem`, `User Interface`, `Text-To-Speech`, `Concurrency`, etc. + +- `doc/src/references/include/` +Bundled include APIs (`form.nvgt`, `menu.nvgt`, `sound_pool.nvgt`, `music.nvgt`, etc.). + +- `doc/src/references/plugin/` +Plugin APIs (`git2nvgt`, `nvgt_curl`, `nvgt_sqlite`, `systemd_notify`). + +## 2. Bundled Script Includes + +- `release/include/` +Reference implementation of bundled `.nvgt` includes shipped with NVGT. +Use this to verify behavior details not obvious in docs. + +Key files: +- `release/include/form.nvgt` +- `release/include/menu.nvgt` +- `release/include/sound_pool.nvgt` +- `release/include/virtual_dialogs.nvgt` +- `release/include/music.nvgt` +- `release/include/speech.nvgt` +- `release/include/bgt_dynamic_menu.nvgt` + +## 3. Engine Binding Source (Truth Layer) + +- `src/*.cpp` +C++ registration and implementation of script-facing APIs. +Use this when docs and includes do not fully answer behavior/signature questions. + +High-value files: +- `src/nvgt.cpp` +CLI modes/options (`-c`, `-d`, include dirs, platform flags, settings). +- `src/scriptstuff.cpp` +Script module/function APIs, profiling/debug hooks, call stack support. +- `src/tts.cpp` +Screen reader + TTS object registration. +- `src/sound.cpp`, `src/sound_nodes.cpp`, `src/sound_service.cpp` +Sound and audio subsystem behavior. +- `src/input.cpp`, `src/UI.cpp` +Keyboard/window/UI interaction behavior. +- `src/filesystem.cpp`, `src/datastreams.cpp`, `src/serialize.cpp` +File/stream/serialization behavior. + +## 4. Samples + +- `extra/samples/` +Small scripts demonstrating typical usage. +Use to confirm common idioms before inventing new patterns. + +## 5. Build and Packaging + +- `readme.md` +Engine build prerequisites and SCons usage. +- `doc/src/manual/compiling your project for distribution.md` +Compile + bundle guidance. +- `build/`, `install/`, `release/` +Build scripts and packaging infrastructure. diff --git a/skills/nvgt-engine-dev/scripts/nvgt_api_report.py b/skills/nvgt-engine-dev/scripts/nvgt_api_report.py new file mode 100755 index 0000000..9ddcf80 --- /dev/null +++ b/skills/nvgt-engine-dev/scripts/nvgt_api_report.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +"""Generate a signature-focused NVGT API evidence report for a symbol.""" + +from __future__ import annotations + +import argparse +import re +import shutil +import subprocess +import sys +from pathlib import Path + + +def discover_nvgt_root(explicit_root: str) -> Path | None: + if explicit_root: + root = Path(explicit_root).expanduser().resolve() + return root if root.exists() else None + + default_root = Path.home() / "git" / "nvgt" + default_resolved = default_root.expanduser().resolve() + if default_resolved.exists(): + return default_resolved + return None + + +def run_rg(pattern: str, paths: list[Path], max_count: int) -> str: + existing = [str(path) for path in paths if path.exists()] + if not existing: + return "" + + cmd = [ + "rg", + "-n", + "--hidden", + "--no-ignore", + "--max-count", + str(max_count), + pattern, + *existing, + ] + proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + if proc.returncode in (0, 1): + return proc.stdout.strip() + raise RuntimeError(proc.stderr.strip() or "ripgrep failed") + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Create an NVGT API evidence report for a symbol.") + parser.add_argument("symbol", help="Function/object/symbol to inspect") + parser.add_argument("--root", default="", help="Path to NVGT root (default: ~/git/nvgt)") + parser.add_argument("--max-count", type=int, default=25, help="Max matches per section (default: 25)") + return parser.parse_args() + + +def main() -> int: + if shutil.which("rg") is None: + print("Error: ripgrep (rg) is required.", file=sys.stderr) + return 2 + + args = parse_args() + nvgt_root = discover_nvgt_root(args.root) + if nvgt_root is None: + print("Error: NVGT root not found at ~/git/nvgt. Pass --root to override.", file=sys.stderr) + return 2 + + escaped_symbol = re.escape(args.symbol) + docs_pattern = rf"\b{escaped_symbol}\b" + include_pattern = docs_pattern + bindings_pattern = rf"Register(GlobalFunction|ObjectMethod|ObjectProperty|Funcdef).*\b{escaped_symbol}\b" + sample_pattern = docs_pattern + + sections = [ + ("docs-references", docs_pattern, [nvgt_root / "doc/src/references"]), + ("include-wrappers", include_pattern, [nvgt_root / "release/include"]), + ("bindings-signatures", bindings_pattern, [nvgt_root / "src"]), + ("samples", sample_pattern, [nvgt_root / "extra/samples"]), + ] + + any_match = False + print(f"NVGT root: {nvgt_root}") + print(f"Symbol: {args.symbol}") + for title, pattern, paths in sections: + output = run_rg(pattern, paths, args.max_count) + if output: + any_match = True + print(f"\n== {title} ==") + print(output) + + if not any_match: + print("\nNo matches found in evidence sections.") + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/skills/nvgt-engine-dev/scripts/nvgt_lookup.py b/skills/nvgt-engine-dev/scripts/nvgt_lookup.py new file mode 100755 index 0000000..6e6073b --- /dev/null +++ b/skills/nvgt-engine-dev/scripts/nvgt_lookup.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +"""Search NVGT docs/includes/bindings in one command.""" + +from __future__ import annotations + +import argparse +import shutil +import subprocess +import sys +from pathlib import Path + + +SECTIONS = { + "docs": ("doc/src",), + "include-docs": ("doc/src/references/include",), + "builtin-docs": ("doc/src/references/builtin",), + "plugin-docs": ("doc/src/references/plugin",), + "release-includes": ("release/include",), + "bindings": ("src",), + "samples": ("extra/samples",), +} + + +def run_rg(query: str, roots: list[Path], max_count: int) -> tuple[int, str]: + existing = [str(p) for p in roots if p.exists()] + if not existing: + return 0, "" + cmd = [ + "rg", + "-n", + "--hidden", + "--no-ignore", + "--max-count", + str(max_count), + query, + *existing, + ] + proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + # ripgrep exits 1 when no matches; that is not an error for this tool. + if proc.returncode in (0, 1): + return proc.returncode, proc.stdout.strip() + raise RuntimeError(proc.stderr.strip() or "ripgrep failed") + + +def resolve_roots(nvgt_root: Path, section: str | None) -> list[tuple[str, list[Path]]]: + if section: + rels = SECTIONS[section] + return [(section, [nvgt_root / rel for rel in rels])] + + groups: list[tuple[str, list[Path]]] = [] + for key, rels in SECTIONS.items(): + groups.append((key, [nvgt_root / rel for rel in rels])) + return groups + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Search NVGT docs/includes/source quickly.") + parser.add_argument("query", help="Symbol or text to search for") + parser.add_argument( + "--root", + default="", + help="Path to NVGT repository root (auto-discovered if omitted)", + ) + parser.add_argument( + "--section", + choices=sorted(SECTIONS.keys()), + help="Limit search to one section", + ) + parser.add_argument( + "--max-count", + type=int, + default=50, + help="Maximum matches per section (default: 50)", + ) + return parser.parse_args() + + +def discover_nvgt_root(explicit_root: str) -> Path | None: + if explicit_root: + root = Path(explicit_root).expanduser().resolve() + return root if root.exists() else None + + default_root = Path.home() / "git" / "nvgt" + default_resolved = default_root.expanduser().resolve() + if default_resolved.exists(): + return default_resolved + return None + + +def main() -> int: + if shutil.which("rg") is None: + print("Error: ripgrep (rg) is required.", file=sys.stderr) + return 2 + + args = parse_args() + nvgt_root = discover_nvgt_root(args.root) + if nvgt_root is None: + print( + "Error: NVGT root not found at ~/git/nvgt. Pass --root to override.", + file=sys.stderr, + ) + return 2 + + groups = resolve_roots(nvgt_root, args.section) + any_match = False + for title, roots in groups: + code, output = run_rg(args.query, roots, args.max_count) + if code == 0 and output: + any_match = True + print(f"\n== {title} ==") + print(output) + + if not any_match: + print("No matches found.") + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/sounds/enemies/draugrhaugr1.ogg b/sounds/enemies/draugrhaugr1.ogg new file mode 100644 index 0000000..94a3edb --- /dev/null +++ b/sounds/enemies/draugrhaugr1.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:43790dd9ac1df6f76e9cd931989b9a5bd3811416b79113359f1661a143e66288 +size 148352 diff --git a/sounds/terrain/draugrhaugr_step.ogg b/sounds/terrain/draugrhaugr_step.ogg new file mode 100644 index 0000000..1880846 --- /dev/null +++ b/sounds/terrain/draugrhaugr_step.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dd90124d4e3e62859a7a4dc08b6bab1771a9038e622c23c4e9d1f921e5311c05 +size 23178 diff --git a/src/audio_utils.nvgt b/src/audio_utils.nvgt index 3bce43e..a84d7b9 100644 --- a/src/audio_utils.nvgt +++ b/src/audio_utils.nvgt @@ -1,3 +1,5 @@ +#include "libstorm-nvgt/volume_controls.nvgt" + bool audio_asset_exists(const string& in soundFile) { if (file_exists(soundFile)) { return true; @@ -190,51 +192,29 @@ void play_player_damage_sound() { // Safe sound handle cleanup - checks if handle is valid and sound is active before destroying void safe_destroy_sound(int& inout handle) { - if (handle != -1) { - if (p.sound_is_active(handle)) { - p.destroy_sound(handle); - } - handle = -1; - } + safe_destroy_sound_in_pool(p, handle); } float master_volume_db = MASTER_VOLUME_MAX_DB; +void apply_master_volume_from_controls(float volumeDb) { + master_volume_db = volumeDb; + sound_master_volume = volumeDb; +} + void init_master_volume() { - master_volume_db = MASTER_VOLUME_MAX_DB; - sound_master_volume = master_volume_db; + volume_controls_set_apply_callback(apply_master_volume_from_controls); + volume_controls_configure(MASTER_VOLUME_MIN_DB, MASTER_VOLUME_MAX_DB, MASTER_VOLUME_STEP_DB, MASTER_VOLUME_MAX_DB); + volume_controls_set_current_db(MASTER_VOLUME_MAX_DB, false); + master_volume_db = volume_controls_get_current_db(); } void set_game_master_volume_db(float volume_db, bool announce = true) { - float clamped = volume_db; - if (clamped > MASTER_VOLUME_MAX_DB) - clamped = MASTER_VOLUME_MAX_DB; - if (clamped < MASTER_VOLUME_MIN_DB) - clamped = MASTER_VOLUME_MIN_DB; - - if (clamped == master_volume_db) - return; - - master_volume_db = clamped; - sound_master_volume = master_volume_db; - - if (announce) { - float range = MASTER_VOLUME_MAX_DB - MASTER_VOLUME_MIN_DB; - float normalized = (master_volume_db - MASTER_VOLUME_MIN_DB) / range; - int volumePercent = int(normalized * 100.0f + 0.5f); - if (volumePercent < 0) - volumePercent = 0; - if (volumePercent > 100) - volumePercent = 100; - screen_reader_speak("Volume " + volumePercent + ".", true); - } + volume_controls_set_current_db(volume_db, announce); + master_volume_db = volume_controls_get_current_db(); } void handle_global_volume_keys() { - if (key_pressed(KEY_PAGEDOWN)) { - set_game_master_volume_db(master_volume_db - MASTER_VOLUME_STEP_DB); - } - if (key_pressed(KEY_PAGEUP)) { - set_game_master_volume_db(master_volume_db + MASTER_VOLUME_STEP_DB); - } + volume_controls_handle_keys(KEY_PAGEDOWN, KEY_PAGEUP, true); + master_volume_db = volume_controls_get_current_db(); } diff --git a/src/bosses/adventure_system.nvgt b/src/bosses/adventure_system.nvgt index 7f1e3ec..b45a654 100644 --- a/src/bosses/adventure_system.nvgt +++ b/src/bosses/adventure_system.nvgt @@ -40,6 +40,8 @@ void run_adventure_menu(int player_x) { adventure_ids.insert_last(ADVENTURE_BANDIT_HIDEOUT); } + i18n_translate_string_array_in_place(options); + if (options.length() == 0) { speak_with_history("No adventures found in this area.", true); return; diff --git a/src/bosses/bandit_hideout.nvgt b/src/bosses/bandit_hideout.nvgt index 6cafc6c..8704901 100644 --- a/src/bosses/bandit_hideout.nvgt +++ b/src/bosses/bandit_hideout.nvgt @@ -302,10 +302,11 @@ void run_bandit_hideout_adventure() { intro.insert_last("The base lies far to the east, guarded by a barricade."); intro.insert_last(""); intro.insert_last("Objective:"); - intro.insert_last(" - Reach the base and destroy the barricade"); + intro.insert_last("- Reach the base and destroy the barricade"); intro.insert_last(""); intro.insert_last("Bandits will, of course, not take this lying down."); - text_reader_lines(intro, "Adventure", true); + i18n_translate_string_array_in_place(intro); + text_reader_lines(intro, i18n_text("Adventure"), true); begin_pet_adventure(@pet_find_hideout_target, @pet_damage_hideout_target, hideoutPlayerX); @@ -522,7 +523,7 @@ void update_hideout_search() { void perform_hideout_search() { if (random(1, 100) <= 10) { - speak_with_history("Found nothing.", true); + speak_with_history(tr("system.search.found_nothing"), true); return; } @@ -533,7 +534,7 @@ void perform_hideout_search() { } } - speak_with_history("Found nothing.", true); + speak_with_history(tr("system.search.found_nothing"), true); } int get_hideout_melee_weapon_type() { @@ -763,7 +764,7 @@ int add_hideout_storage_item(int itemType, int amount) { if (itemType == ITEM_SMALL_GAME) { for (int i = 0; i < addedAmount; i++) { - storage_small_game_types.insert_last("small game"); + storage_small_game_types.insert_last(i18n_text("small game")); } } if (itemType == ITEM_FISH) { @@ -856,6 +857,7 @@ void give_bandit_hideout_rewards() { } } - text_reader_lines(rewards, "Bandit's Hideout", true); + i18n_translate_string_array_in_place(rewards); + text_reader_lines(rewards, i18n_text("Bandit's Hideout"), true); attempt_pet_offer_from_adventure(); } diff --git a/src/bosses/unicorn/unicorn_boss.nvgt b/src/bosses/unicorn/unicorn_boss.nvgt index 00211fe..4aca8bd 100644 --- a/src/bosses/unicorn/unicorn_boss.nvgt +++ b/src/bosses/unicorn/unicorn_boss.nvgt @@ -110,14 +110,15 @@ void run_unicorn_adventure() { intro.insert_last("that can be destroyed with an axe."); intro.insert_last(""); intro.insert_last("Strategy:"); - intro.insert_last(" - Use your axe to destroy a bridge support"); - intro.insert_last(" - Lure the Unicorn onto the bridge"); - intro.insert_last(" - Or fight the Unicorn directly (it has massive health)"); - intro.insert_last(" - Jump (UP arrow) to avoid being trampled"); - intro.insert_last(" - When the bridge collapses with the Unicorn on it, you win!"); + intro.insert_last("- Use your axe to destroy a bridge support"); + intro.insert_last("- Lure the Unicorn onto the bridge"); + intro.insert_last("- Or fight the Unicorn directly (it has massive health)"); + intro.insert_last("- Jump (UP arrow) to avoid being trampled"); + intro.insert_last("- When the bridge collapses with the Unicorn on it, you win!"); intro.insert_last(""); intro.insert_last("Controls: LEFT/RIGHT to move, UP to jump, CTRL to attack, ESC to flee"); - text_reader_lines(intro, "Adventure", true); + i18n_translate_string_array_in_place(intro); + text_reader_lines(intro, i18n_text("Adventure"), true); begin_pet_adventure(@pet_find_unicorn_target, @pet_damage_unicorn_target, player_arena_x); @@ -723,6 +724,7 @@ void give_unicorn_rewards() { append_adventure_completion_rewards(ADVENTURE_UNICORN, rewards); // Display rewards in text reader - text_reader_lines(rewards, "Unicorn Victory", true); + i18n_translate_string_array_in_place(rewards); + text_reader_lines(rewards, i18n_text("Unicorn Victory"), true); attempt_pet_offer_from_adventure(); } diff --git a/src/crafting/craft_barricade.nvgt b/src/crafting/craft_barricade.nvgt index 57ec5f5..1259818 100644 --- a/src/crafting/craft_barricade.nvgt +++ b/src/crafting/craft_barricade.nvgt @@ -1,4 +1,11 @@ // Crafting barricade reinforcements +string get_barricade_option_text(const string& in key, int cost, int health) { + dictionary args; + args.set("cost", cost); + args.set("health", health); + return trf(key, args); +} + void run_barricade_menu() { if (barricade_health >= BARRICADE_MAX_HEALTH) { speak_with_history("Barricade is already at full health.", true); @@ -10,23 +17,24 @@ void run_barricade_menu() { int[] action_types; // 0 = sticks, 1 = vines, 2 = log, 3 = stones if (get_personal_count(ITEM_STICKS) >= BARRICADE_STICK_COST) { - options.insert_last("Reinforce with sticks (" + BARRICADE_STICK_COST + " sticks, +" + BARRICADE_STICK_HEALTH + - " health)"); + options.insert_last(get_barricade_option_text("system.crafting.barricade.option.reinforce_sticks", + BARRICADE_STICK_COST, BARRICADE_STICK_HEALTH)); action_types.insert_last(0); } if (get_personal_count(ITEM_VINES) >= BARRICADE_VINE_COST) { - options.insert_last("Reinforce with vines (" + BARRICADE_VINE_COST + " vines, +" + BARRICADE_VINE_HEALTH + - " health)"); + options.insert_last(get_barricade_option_text("system.crafting.barricade.option.reinforce_vines", + BARRICADE_VINE_COST, BARRICADE_VINE_HEALTH)); action_types.insert_last(1); } if (get_personal_count(ITEM_LOGS) >= BARRICADE_LOG_COST) { - options.insert_last("Reinforce with log (" + BARRICADE_LOG_COST + " log, +" + BARRICADE_LOG_HEALTH + - " health)"); + options.insert_last( + get_barricade_option_text("system.crafting.barricade.option.reinforce_log", BARRICADE_LOG_COST, + BARRICADE_LOG_HEALTH)); action_types.insert_last(2); } if (get_personal_count(ITEM_STONES) >= BARRICADE_STONE_COST) { - options.insert_last("Reinforce with stones (" + BARRICADE_STONE_COST + " stones, +" + BARRICADE_STONE_HEALTH + - " health)"); + options.insert_last(get_barricade_option_text("system.crafting.barricade.option.reinforce_stones", + BARRICADE_STONE_COST, BARRICADE_STONE_HEALTH)); action_types.insert_last(3); } @@ -34,7 +42,7 @@ void run_barricade_menu() { speak_with_history("No materials to reinforce the barricade.", true); return; } - speak_with_history("Barricade. " + options[selection], true); + speak_menu_prompt("system.crafting.barricade.prompt", options[selection]); while (true) { wait(5); @@ -171,7 +179,7 @@ void reinforce_barricade_max_with_sticks() { } if (get_personal_count(ITEM_STICKS) < BARRICADE_STICK_COST) { - speak_with_history("Missing: " + BARRICADE_STICK_COST + " sticks", true); + speak_crafting_missing(BARRICADE_STICK_COST + " sticks"); return; } @@ -200,7 +208,7 @@ void reinforce_barricade_max_with_vines() { } if (get_personal_count(ITEM_VINES) < BARRICADE_VINE_COST) { - speak_with_history("Missing: " + BARRICADE_VINE_COST + " vines", true); + speak_crafting_missing(BARRICADE_VINE_COST + " vines"); return; } @@ -229,7 +237,7 @@ void reinforce_barricade_max_with_log() { } if (get_personal_count(ITEM_LOGS) < BARRICADE_LOG_COST) { - speak_with_history("Missing: " + BARRICADE_LOG_COST + " log", true); + speak_crafting_missing(BARRICADE_LOG_COST + " log"); return; } @@ -257,7 +265,7 @@ void reinforce_barricade_max_with_stones() { } if (get_personal_count(ITEM_STONES) < BARRICADE_STONE_COST) { - speak_with_history("Missing: " + BARRICADE_STONE_COST + " stones", true); + speak_crafting_missing(BARRICADE_STONE_COST + " stones"); return; } diff --git a/src/crafting/craft_buildings.nvgt b/src/crafting/craft_buildings.nvgt index fce9c93..7853152 100644 --- a/src/crafting/craft_buildings.nvgt +++ b/src/crafting/craft_buildings.nvgt @@ -55,44 +55,44 @@ void run_buildings_menu() { // Firepit and Fire are always available outside base, // but only one of each can exist in base. if (x > BASE_END || !base_has_firepit) { - options.insert_last("Firepit (9 Stones)"); + options.insert_last(tr("system.crafting.buildings.option.firepit")); building_types.insert_last(0); } if (x > BASE_END || !base_has_fire) { - options.insert_last("Fire (2 Sticks, 1 Log) [Requires Firepit]"); + options.insert_last(tr("system.crafting.buildings.option.fire")); building_types.insert_last(1); } // Only show herb garden if not already built in base if (get_herb_garden_at_base() == null) { - options.insert_last("Herb Garden (9 Stones, 3 Vines, 2 Logs) [Base Only]"); + options.insert_last(tr("system.crafting.buildings.option.herb_garden")); building_types.insert_last(2); } // Storage upgrades if (storage_level < STORAGE_LEVEL_UPGRADE_1) { - options.insert_last("Upgrade Storage (6 Logs, 9 Stones, 8 Vines) [Base Only, 50 each]"); + options.insert_last(tr("system.crafting.buildings.option.storage_upgrade_1")); building_types.insert_last(3); } else if (storage_level == STORAGE_LEVEL_UPGRADE_1) { - options.insert_last("Upgrade Storage (12 Logs, 18 Stones, 16 Vines) [Base Only, 100 each]"); + options.insert_last(tr("system.crafting.buildings.option.storage_upgrade_2")); building_types.insert_last(3); } // Only show pasture if not built and storage is upgraded if (world_pastures.length() == 0 && storage_level >= STORAGE_LEVEL_UPGRADE_1) { - options.insert_last("Pasture (8 Logs, 18 Ropes) [Base Only, Requires Storage Upgrade]"); + options.insert_last(tr("system.crafting.buildings.option.pasture")); building_types.insert_last(4); } // Only show stable if not built and storage is upgraded if (world_stables.length() == 0 && storage_level >= STORAGE_LEVEL_UPGRADE_1) { - options.insert_last("Stable (10 Logs, 15 Stones, 10 Vines) [Base Only, Requires Storage Upgrade]"); + options.insert_last(tr("system.crafting.buildings.option.stable")); building_types.insert_last(5); } // Only show altar if not built if (world_altars.length() == 0) { - options.insert_last("Altar (9 Stones, 3 Sticks) [Base Only]"); + options.insert_last(tr("system.crafting.buildings.option.altar")); building_types.insert_last(6); } @@ -100,7 +100,7 @@ void run_buildings_menu() { speak_with_history("No buildings available.", true); return; } - speak_with_history("Buildings. " + options[selection], true); + speak_menu_prompt("system.crafting.buildings.prompt", options[selection]); while (true) { wait(5); @@ -177,7 +177,7 @@ void craft_firepit() { add_world_firepit(x); speak_with_history("Firepit built here.", true); } else { - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); } } @@ -213,7 +213,7 @@ void craft_campfire() { add_world_fire(firepit.position); speak_with_history("Fire built at firepit.", true); } else { - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); } } @@ -246,7 +246,7 @@ void craft_herb_garden() { add_world_herb_garden(x); speak_with_history("Herb garden built. The base now heals faster.", true); } else { - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); } } @@ -292,7 +292,7 @@ void craft_storage() { storage_level = targetLevel; speak_with_history("Storage upgraded. Capacity is now " + newCapacity + " per item.", true); } else { - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); } } @@ -322,7 +322,7 @@ void craft_pasture() { add_world_pasture(x); speak_with_history("Pasture built.", true); } else { - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); } } @@ -355,7 +355,7 @@ void craft_stable() { add_world_stable(x); speak_with_history("Stable built.", true); } else { - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); } } @@ -381,6 +381,6 @@ void craft_altar() { add_world_altar(x); speak_with_history("Altar built.", true); } else { - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); } } diff --git a/src/crafting/craft_clothing.nvgt b/src/crafting/craft_clothing.nvgt index f1b410b..d662dcb 100644 --- a/src/crafting/craft_clothing.nvgt +++ b/src/crafting/craft_clothing.nvgt @@ -44,14 +44,14 @@ void consume_pouches(int amount) { void run_clothing_menu() { int selection = 0; - string[] options = {"Skin Hat (1 Skin, 1 Vine)", - "Skin Gloves (1 Skin, 1 Vine)", - "Skin Pants (6 Skins, 3 Vines)", - "Skin Tunic (4 Skins, 2 Vines)", - "Moccasins (2 Skins, 1 Vine)", - "Skin Pouch (2 Skins, 1 Vine)", - "Backpack (11 Skins, 5 Vines, 4 Skin Pouches)"}; - speak_with_history("Clothing. " + options[selection], true); + string[] options = {tr("system.crafting.clothing.option.skin_hat"), + tr("system.crafting.clothing.option.skin_gloves"), + tr("system.crafting.clothing.option.skin_pants"), + tr("system.crafting.clothing.option.skin_tunic"), + tr("system.crafting.clothing.option.moccasins"), + tr("system.crafting.clothing.option.skin_pouch"), + tr("system.crafting.clothing.option.backpack")}; + speak_menu_prompt("system.crafting.clothing.prompt", options[selection]); while (true) { wait(5); @@ -128,7 +128,7 @@ void craft_skin_hat() { if (missing == "") { if (get_personal_count(ITEM_SKIN_HATS) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more skin hats.", true); + speak_cant_carry_any_more_item(ITEM_SKIN_HATS); return; } simulate_crafting(2); @@ -137,13 +137,13 @@ void craft_skin_hat() { add_personal_count(ITEM_SKIN_HATS, 1); speak_with_history("Crafted a Skin Hat.", true); } else { - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); } } void craft_skin_hat_max() { if (get_personal_count(ITEM_SKIN_HATS) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more skin hats.", true); + speak_cant_carry_any_more_item(ITEM_SKIN_HATS); return; } @@ -163,7 +163,7 @@ void craft_skin_hat_max() { missing += "1 skin "; if (get_personal_count(ITEM_VINES) < 1) missing += "1 vine "; - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); return; } @@ -185,7 +185,7 @@ void craft_skin_gloves() { if (missing == "") { if (get_personal_count(ITEM_SKIN_GLOVES) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more skin gloves.", true); + speak_cant_carry_any_more_item(ITEM_SKIN_GLOVES); return; } simulate_crafting(2); @@ -194,13 +194,13 @@ void craft_skin_gloves() { add_personal_count(ITEM_SKIN_GLOVES, 1); speak_with_history("Crafted Skin Gloves.", true); } else { - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); } } void craft_skin_gloves_max() { if (get_personal_count(ITEM_SKIN_GLOVES) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more skin gloves.", true); + speak_cant_carry_any_more_item(ITEM_SKIN_GLOVES); return; } @@ -220,7 +220,7 @@ void craft_skin_gloves_max() { missing += "1 skin "; if (get_personal_count(ITEM_VINES) < 1) missing += "1 vine "; - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); return; } @@ -242,7 +242,7 @@ void craft_skin_pants() { if (missing == "") { if (get_personal_count(ITEM_SKIN_PANTS) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more skin pants.", true); + speak_cant_carry_any_more_item(ITEM_SKIN_PANTS); return; } simulate_crafting(9); @@ -251,13 +251,13 @@ void craft_skin_pants() { add_personal_count(ITEM_SKIN_PANTS, 1); speak_with_history("Crafted Skin Pants.", true); } else { - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); } } void craft_skin_pants_max() { if (get_personal_count(ITEM_SKIN_PANTS) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more skin pants.", true); + speak_cant_carry_any_more_item(ITEM_SKIN_PANTS); return; } @@ -277,7 +277,7 @@ void craft_skin_pants_max() { missing += "6 skins "; if (get_personal_count(ITEM_VINES) < 3) missing += "3 vines "; - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); return; } @@ -298,7 +298,7 @@ void craft_skin_tunic() { if (missing == "") { if (get_personal_count(ITEM_SKIN_TUNICS) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more skin tunics.", true); + speak_cant_carry_any_more_item(ITEM_SKIN_TUNICS); return; } simulate_crafting(6); @@ -307,13 +307,13 @@ void craft_skin_tunic() { add_personal_count(ITEM_SKIN_TUNICS, 1); speak_with_history("Crafted a Skin Tunic.", true); } else { - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); } } void craft_skin_tunic_max() { if (get_personal_count(ITEM_SKIN_TUNICS) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more skin tunics.", true); + speak_cant_carry_any_more_item(ITEM_SKIN_TUNICS); return; } @@ -333,7 +333,7 @@ void craft_skin_tunic_max() { missing += "4 skins "; if (get_personal_count(ITEM_VINES) < 2) missing += "2 vines "; - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); return; } @@ -354,7 +354,7 @@ void craft_moccasins() { if (missing == "") { if (get_personal_count(ITEM_MOCCASINS) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more moccasins.", true); + speak_cant_carry_any_more_item(ITEM_MOCCASINS); return; } simulate_crafting(3); @@ -363,13 +363,13 @@ void craft_moccasins() { add_personal_count(ITEM_MOCCASINS, 1); speak_with_history("Crafted moccasins.", true); } else { - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); } } void craft_moccasins_max() { if (get_personal_count(ITEM_MOCCASINS) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more moccasins.", true); + speak_cant_carry_any_more_item(ITEM_MOCCASINS); return; } @@ -389,7 +389,7 @@ void craft_moccasins_max() { missing += "2 skins "; if (get_personal_count(ITEM_VINES) < 1) missing += "1 vine "; - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); return; } @@ -411,7 +411,7 @@ void craft_skin_pouch() { if (missing == "") { if (get_personal_count(ITEM_SKIN_POUCHES) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more skin pouches.", true); + speak_cant_carry_any_more_item(ITEM_SKIN_POUCHES); return; } simulate_crafting(3); @@ -420,13 +420,13 @@ void craft_skin_pouch() { add_personal_count(ITEM_SKIN_POUCHES, 1); speak_with_history("Crafted a Skin Pouch.", true); } else { - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); } } void craft_skin_pouch_max() { if (get_personal_count(ITEM_SKIN_POUCHES) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more skin pouches.", true); + speak_cant_carry_any_more_item(ITEM_SKIN_POUCHES); return; } @@ -446,7 +446,7 @@ void craft_skin_pouch_max() { missing += "2 skins "; if (get_personal_count(ITEM_VINES) < 1) missing += "1 vine "; - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); return; } @@ -470,7 +470,7 @@ void craft_backpack() { if (missing == "") { if (get_personal_count(ITEM_BACKPACKS) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more backpacks.", true); + speak_cant_carry_any_more_item(ITEM_BACKPACKS); return; } simulate_crafting(20); @@ -480,13 +480,13 @@ void craft_backpack() { add_personal_count(ITEM_BACKPACKS, 1); speak_with_history("Crafted a Backpack.", true); } else { - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); } } void craft_backpack_max() { if (get_personal_count(ITEM_BACKPACKS) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more backpacks.", true); + speak_cant_carry_any_more_item(ITEM_BACKPACKS); return; } @@ -511,7 +511,7 @@ void craft_backpack_max() { missing += "5 vines "; if (get_total_pouch_count() < 4) missing += "4 skin pouches "; - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); return; } diff --git a/src/crafting/craft_materials.nvgt b/src/crafting/craft_materials.nvgt index 5fa5648..8516b56 100644 --- a/src/crafting/craft_materials.nvgt +++ b/src/crafting/craft_materials.nvgt @@ -1,11 +1,12 @@ // Crafting materials void run_materials_menu() { int selection = 0; - string[] options = { - "Butcher Game [Requires Game, Knife, Fire nearby]", "Smoke Fish (1 Fish, 1 Stick) [Requires Fire nearby]", - "Arrows (2 Sticks, 4 Feathers, 2 Stones) [Requires Quiver]", "Bowstring (3 Sinew) [Requires Fire nearby]", - "Incense (6 Sticks, 2 Vines, 1 Reed) [Requires Altar]"}; - speak_with_history("Materials. " + options[selection], true); + string[] options = {tr("system.crafting.materials.option.butcher_game"), + tr("system.crafting.materials.option.smoke_fish"), + tr("system.crafting.materials.option.arrows"), + tr("system.crafting.materials.option.bowstring"), + tr("system.crafting.materials.option.incense")}; + speak_menu_prompt("system.crafting.materials.prompt", options[selection]); while (true) { wait(5); @@ -94,7 +95,7 @@ void craft_arrows() { add_personal_count(ITEM_ARROWS, ARROWS_PER_CRAFT); speak_with_history("Crafted " + ARROWS_PER_CRAFT + " arrows.", true); } else { - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); } } @@ -131,7 +132,7 @@ void craft_arrows_max() { missing += "4 feathers "; if (get_personal_count(ITEM_STONES) < 2) missing += "2 stones "; - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); return; } @@ -148,7 +149,7 @@ void craft_arrows_max() { void craft_bowstring() { WorldFire @fire = get_fire_within_range(x, 3); if (fire == null) { - speak_with_history("You need a fire within 3 tiles to make bowstring.", true); + speak_with_history(tr("system.crafting.require.fire_within_three_bowstring"), true); return; } @@ -158,7 +159,7 @@ void craft_bowstring() { if (missing == "") { if (get_personal_count(ITEM_BOWSTRINGS) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more bowstrings.", true); + speak_cant_carry_any_more_item(ITEM_BOWSTRINGS); return; } simulate_crafting(3); @@ -166,19 +167,19 @@ void craft_bowstring() { add_personal_count(ITEM_BOWSTRINGS, 1); speak_with_history("Crafted a bowstring.", true); } else { - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); } } void craft_bowstring_max() { WorldFire @fire = get_fire_within_range(x, 3); if (fire == null) { - speak_with_history("You need a fire within 3 tiles to make bowstring.", true); + speak_with_history(tr("system.crafting.require.fire_within_three_bowstring"), true); return; } if (get_personal_count(ITEM_BOWSTRINGS) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more bowstrings.", true); + speak_cant_carry_any_more_item(ITEM_BOWSTRINGS); return; } @@ -190,7 +191,7 @@ void craft_bowstring_max() { max_craft = space; if (max_craft <= 0) { - speak_with_history("Missing: 3 sinew", true); + speak_crafting_missing("3 sinew"); return; } @@ -204,7 +205,7 @@ void craft_bowstring_max() { void craft_incense() { if (world_altars.length() == 0) { - speak_with_history("You need an altar to craft incense.", true); + speak_with_history(tr("system.crafting.require.altar_incense"), true); return; } @@ -218,7 +219,7 @@ void craft_incense() { if (missing == "") { if (get_personal_count(ITEM_INCENSE) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more incense.", true); + speak_cant_carry_any_more_item(ITEM_INCENSE); return; } simulate_crafting(INCENSE_STICK_COST + INCENSE_VINE_COST + INCENSE_REED_COST); @@ -228,18 +229,18 @@ void craft_incense() { add_personal_count(ITEM_INCENSE, 1); speak_with_history("Crafted incense.", true); } else { - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); } } void craft_incense_max() { if (world_altars.length() == 0) { - speak_with_history("You need an altar to craft incense.", true); + speak_with_history(tr("system.crafting.require.altar_incense"), true); return; } if (get_personal_count(ITEM_INCENSE) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more incense.", true); + speak_cant_carry_any_more_item(ITEM_INCENSE); return; } @@ -264,7 +265,7 @@ void craft_incense_max() { missing += INCENSE_VINE_COST + " vines "; if (get_personal_count(ITEM_REEDS) < INCENSE_REED_COST) missing += INCENSE_REED_COST + " reed "; - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); return; } @@ -281,7 +282,7 @@ void craft_incense_max() { void craft_smoke_fish() { WorldFire @fire = get_fire_within_range(x, 3); if (fire == null) { - speak_with_history("You need a fire within 3 tiles to smoke fish.", true); + speak_with_history(tr("system.crafting.require.fire_within_three_smoke_fish"), true); return; } @@ -296,7 +297,7 @@ void craft_smoke_fish() { int yield = get_smoked_fish_yield(weight); int space = get_personal_stack_limit() - get_personal_count(ITEM_SMOKED_FISH); if (space < yield) { - speak_with_history("You can't carry any more smoked fish.", true); + speak_cant_carry_any_more_item(ITEM_SMOKED_FISH); return; } simulate_crafting(2); @@ -306,14 +307,14 @@ void craft_smoke_fish() { add_personal_count(ITEM_SMOKED_FISH, yield); speak_with_history("Smoked a fish into " + yield + " smoked fish.", true); } else { - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); } } void craft_smoke_fish_max() { WorldFire @fire = get_fire_within_range(x, 3); if (fire == null) { - speak_with_history("You need a fire within 3 tiles to smoke fish.", true); + speak_with_history(tr("system.crafting.require.fire_within_three_smoke_fish"), true); return; } @@ -329,13 +330,13 @@ void craft_smoke_fish_max() { missing += "1 fish "; if (get_personal_count(ITEM_STICKS) < 1) missing += "1 stick "; - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); return; } int space = get_personal_stack_limit() - get_personal_count(ITEM_SMOKED_FISH); if (space <= 0) { - speak_with_history("You can't carry any more smoked fish.", true); + speak_cant_carry_any_more_item(ITEM_SMOKED_FISH); return; } @@ -351,7 +352,7 @@ void craft_smoke_fish_max() { } if (fish_to_smoke <= 0) { - speak_with_history("You can't carry any more smoked fish.", true); + speak_cant_carry_any_more_item(ITEM_SMOKED_FISH); return; } @@ -381,17 +382,17 @@ void butcher_small_game() { // Check for fire within 3 tiles (can hear it) WorldFire @fire = get_fire_within_range(x, 3); if (fire == null) { - speak_with_history("You need a fire within 3 tiles to butcher.", true); + speak_with_history(tr("system.crafting.require.fire_within_three_butcher"), true); return; } if (missing == "") { if (get_personal_count(ITEM_MEAT) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more meat.", true); + speak_cant_carry_any_more_item(ITEM_MEAT); return; } if (get_personal_count(ITEM_SKINS) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more skins.", true); + speak_cant_carry_any_more_item(ITEM_SKINS); return; } simulate_crafting(1); @@ -429,7 +430,7 @@ void butcher_small_game() { // Play sound p.play_stationary("sounds/items/miscellaneous.ogg", false); } else { - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); } } @@ -438,20 +439,20 @@ void butcher_small_game_max() { // Check for knife if (get_personal_count(ITEM_KNIVES) < 1) { - speak_with_history("Missing: Stone Knife", true); + speak_crafting_missing("Stone Knife"); return; } // Check for small game or boar if (get_personal_count(ITEM_SMALL_GAME) < 1 && get_personal_count(ITEM_BOAR_CARCASSES) < 1) { - speak_with_history("Missing: Game", true); + speak_crafting_missing("Game"); return; } // Check for fire within 3 tiles (can hear it) WorldFire @fire = get_fire_within_range(x, 3); if (fire == null) { - speak_with_history("You need a fire within 3 tiles to butcher.", true); + speak_with_history(tr("system.crafting.require.fire_within_three_butcher"), true); return; } @@ -460,11 +461,11 @@ void butcher_small_game_max() { int skins_space = get_personal_stack_limit() - get_personal_count(ITEM_SKINS); if (meat_space <= 0) { - speak_with_history("You can't carry any more meat.", true); + speak_cant_carry_any_more_item(ITEM_MEAT); return; } if (skins_space <= 0) { - speak_with_history("You can't carry any more skins.", true); + speak_cant_carry_any_more_item(ITEM_SKINS); return; } diff --git a/src/crafting/craft_runes.nvgt b/src/crafting/craft_runes.nvgt index a896bc0..346e17e 100644 --- a/src/crafting/craft_runes.nvgt +++ b/src/crafting/craft_runes.nvgt @@ -5,58 +5,58 @@ // Get the base equipment name without any rune prefix string get_base_equipment_name(int equip_type) { if (equip_type == EQUIP_SPEAR) - return "Spear"; + return i18n_lookup_key_with_fallback("system.equipment.name.spear", "Spear"); if (equip_type == EQUIP_AXE) - return "Stone Axe"; + return i18n_lookup_key_with_fallback("system.equipment.name.stone_axe", "Stone Axe"); if (equip_type == EQUIP_SLING) - return "Sling"; + return i18n_lookup_key_with_fallback("system.equipment.name.sling", "Sling"); if (equip_type == EQUIP_BOW) - return "Bow"; + return i18n_lookup_key_with_fallback("system.equipment.name.bow", "Bow"); if (equip_type == EQUIP_HAT) - return "Skin Hat"; + return i18n_lookup_key_with_fallback("system.equipment.name.skin_hat", "Skin Hat"); if (equip_type == EQUIP_GLOVES) - return "Skin Gloves"; + return i18n_lookup_key_with_fallback("system.equipment.name.skin_gloves", "Skin Gloves"); if (equip_type == EQUIP_PANTS) - return "Skin Pants"; + return i18n_lookup_key_with_fallback("system.equipment.name.skin_pants", "Skin Pants"); if (equip_type == EQUIP_TUNIC) - return "Skin Tunic"; + return i18n_lookup_key_with_fallback("system.equipment.name.skin_tunic", "Skin Tunic"); if (equip_type == EQUIP_MOCCASINS) - return "Moccasins"; + return i18n_lookup_key_with_fallback("system.equipment.name.moccasins", "Moccasins"); if (equip_type == EQUIP_POUCH) - return "Skin Pouch"; + return i18n_lookup_key_with_fallback("system.equipment.name.skin_pouch", "Skin Pouch"); if (equip_type == EQUIP_BACKPACK) - return "Backpack"; + return i18n_lookup_key_with_fallback("system.equipment.name.backpack", "Backpack"); if (equip_type == EQUIP_FISHING_POLE) - return "Fishing Pole"; - return "Unknown"; + return i18n_lookup_key_with_fallback("system.equipment.name.fishing_pole", "Fishing Pole"); + return i18n_lookup_key_with_fallback("system.equipment.name.unknown", "Unknown"); } string get_base_equipment_name_plural(int equip_type) { if (equip_type == EQUIP_SPEAR) - return "Spears"; + return i18n_lookup_key_with_fallback("system.equipment.name_plural.spears", "Spears"); if (equip_type == EQUIP_AXE) - return "Stone Axes"; + return i18n_lookup_key_with_fallback("system.equipment.name_plural.stone_axes", "Stone Axes"); if (equip_type == EQUIP_SLING) - return "Slings"; + return i18n_lookup_key_with_fallback("system.equipment.name_plural.slings", "Slings"); if (equip_type == EQUIP_BOW) - return "Bows"; + return i18n_lookup_key_with_fallback("system.equipment.name_plural.bows", "Bows"); if (equip_type == EQUIP_HAT) - return "Skin Hats"; + return i18n_lookup_key_with_fallback("system.equipment.name_plural.skin_hats", "Skin Hats"); if (equip_type == EQUIP_GLOVES) - return "Skin Gloves"; + return i18n_lookup_key_with_fallback("system.equipment.name_plural.skin_gloves", "Skin Gloves"); if (equip_type == EQUIP_PANTS) - return "Skin Pants"; + return i18n_lookup_key_with_fallback("system.equipment.name_plural.skin_pants", "Skin Pants"); if (equip_type == EQUIP_TUNIC) - return "Skin Tunics"; + return i18n_lookup_key_with_fallback("system.equipment.name_plural.skin_tunics", "Skin Tunics"); if (equip_type == EQUIP_MOCCASINS) - return "Moccasins"; + return i18n_lookup_key_with_fallback("system.equipment.name_plural.moccasins", "Moccasins"); if (equip_type == EQUIP_POUCH) - return "Skin Pouches"; + return i18n_lookup_key_with_fallback("system.equipment.name_plural.skin_pouches", "Skin Pouches"); if (equip_type == EQUIP_BACKPACK) - return "Backpacks"; + return i18n_lookup_key_with_fallback("system.equipment.name_plural.backpacks", "Backpacks"); if (equip_type == EQUIP_FISHING_POLE) - return "Fishing Poles"; - return "Items"; + return i18n_lookup_key_with_fallback("system.equipment.name_plural.fishing_poles", "Fishing Poles"); + return i18n_lookup_key_with_fallback("system.equipment.name_plural.items", "Items"); } // Get inventory count for an equipment type @@ -213,7 +213,7 @@ void run_rune_equipment_menu(int rune_type) { int unruned_count = get_unruned_equipment_count(equip_type); if (unruned_count > 0) { string name = get_base_equipment_name(equip_type); - equipment_options.insert_last(name + " (" + unruned_count + " available)"); + equipment_options.insert_last(i18n_text(name + " (" + unruned_count + " available)")); equipment_types.insert_last(equip_type); } } @@ -302,7 +302,7 @@ void engrave_rune(int equip_type, int rune_type) { string runed_name = "Runed " + get_base_equipment_name(equip_type) + " of " + get_rune_effect_name(rune_type); speak_with_history("Engraved " + runed_name + ".", true); } else { - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); } } @@ -327,7 +327,7 @@ void engrave_rune_max(int equip_type, int rune_type) { max_craft = favor_count; if (missing != "") { - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); return; } @@ -338,7 +338,7 @@ void engrave_rune_max(int equip_type, int rune_type) { missing += "1 favor "; if (missing == "") missing = "resources"; - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); return; } diff --git a/src/crafting/craft_tools.nvgt b/src/crafting/craft_tools.nvgt index 7b8c6f0..2c63228 100644 --- a/src/crafting/craft_tools.nvgt +++ b/src/crafting/craft_tools.nvgt @@ -1,16 +1,16 @@ // Crafting tools void run_tools_menu() { int selection = 0; - string[] options = {"Stone Knife (2 Stones)", - "Snare (1 Stick, 2 Vines)", - "Stone Axe (1 Stick, 1 Vine, 2 Stones) [Requires Knife]", - "Fishing Pole (1 Stick, 2 Vines)", - "Rope (3 Vines)", - "Quiver (2 Skins, 2 Vines)", - "Canoe (4 Logs, 11 Sticks, 11 Vines, 6 Skins, 2 Rope, 6 Reeds)", - "Reed Basket (3 Reeds)", - "Clay Pot (3 Clay)"}; - speak_with_history("Tools. " + options[selection], true); + string[] options = {tr("system.crafting.tools.option.stone_knife"), + tr("system.crafting.tools.option.snare"), + tr("system.crafting.tools.option.stone_axe"), + tr("system.crafting.tools.option.fishing_pole"), + tr("system.crafting.tools.option.rope"), + tr("system.crafting.tools.option.quiver"), + tr("system.crafting.tools.option.canoe"), + tr("system.crafting.tools.option.reed_basket"), + tr("system.crafting.tools.option.clay_pot")}; + speak_menu_prompt("system.crafting.tools.prompt", options[selection]); while (true) { wait(5); @@ -93,7 +93,7 @@ void craft_knife() { if (missing == "") { if (get_personal_count(ITEM_KNIVES) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more stone knives.", true); + speak_cant_carry_any_more_item(ITEM_KNIVES); return; } simulate_crafting(2); @@ -101,13 +101,13 @@ void craft_knife() { add_personal_count(ITEM_KNIVES, 1); speak_with_history("Crafted a Stone Knife.", true); } else { - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); } } void craft_knife_max() { if (get_personal_count(ITEM_KNIVES) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more stone knives.", true); + speak_cant_carry_any_more_item(ITEM_KNIVES); return; } @@ -117,7 +117,7 @@ void craft_knife_max() { max_possible = space; if (max_possible <= 0) { - speak_with_history("Missing: 2 stones", true); + speak_crafting_missing("2 stones"); return; } @@ -138,7 +138,7 @@ void craft_snare() { if (missing == "") { if (get_personal_count(ITEM_SNARES) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more snares.", true); + speak_cant_carry_any_more_item(ITEM_SNARES); return; } simulate_crafting(3); @@ -147,13 +147,13 @@ void craft_snare() { add_personal_count(ITEM_SNARES, 1); speak_with_history("Crafted a Snare.", true); } else { - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); } } void craft_snare_max() { if (get_personal_count(ITEM_SNARES) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more snares.", true); + speak_cant_carry_any_more_item(ITEM_SNARES); return; } @@ -173,7 +173,7 @@ void craft_snare_max() { missing += "1 stick "; if (get_personal_count(ITEM_VINES) < 2) missing += "2 vines "; - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); return; } @@ -195,7 +195,7 @@ void craft_fishing_pole() { if (missing == "") { if (get_personal_count(ITEM_FISHING_POLES) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more fishing poles.", true); + speak_cant_carry_any_more_item(ITEM_FISHING_POLES); return; } simulate_crafting(3); @@ -204,13 +204,13 @@ void craft_fishing_pole() { add_personal_count(ITEM_FISHING_POLES, 1); speak_with_history("Crafted a Fishing Pole.", true); } else { - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); } } void craft_fishing_pole_max() { if (get_personal_count(ITEM_FISHING_POLES) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more fishing poles.", true); + speak_cant_carry_any_more_item(ITEM_FISHING_POLES); return; } @@ -230,7 +230,7 @@ void craft_fishing_pole_max() { missing += "1 stick "; if (get_personal_count(ITEM_VINES) < 2) missing += "2 vines "; - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); return; } @@ -250,7 +250,7 @@ void craft_rope() { if (missing == "") { if (get_personal_count(ITEM_ROPES) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more rope.", true); + speak_cant_carry_any_more_item(ITEM_ROPES); return; } simulate_crafting(3); @@ -258,13 +258,13 @@ void craft_rope() { add_personal_count(ITEM_ROPES, 1); speak_with_history("Crafted rope.", true); } else { - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); } } void craft_rope_max() { if (get_personal_count(ITEM_ROPES) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more rope.", true); + speak_cant_carry_any_more_item(ITEM_ROPES); return; } @@ -275,7 +275,7 @@ void craft_rope_max() { max_craft = space; if (max_craft <= 0) { - speak_with_history("Missing: 3 vines", true); + speak_crafting_missing("3 vines"); return; } @@ -296,7 +296,7 @@ void craft_quiver() { if (missing == "") { if (get_personal_count(ITEM_QUIVERS) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more quivers.", true); + speak_cant_carry_any_more_item(ITEM_QUIVERS); return; } simulate_crafting(4); @@ -305,13 +305,13 @@ void craft_quiver() { add_personal_count(ITEM_QUIVERS, 1); speak_with_history("Crafted a Quiver.", true); } else { - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); } } void craft_quiver_max() { if (get_personal_count(ITEM_QUIVERS) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more quivers.", true); + speak_cant_carry_any_more_item(ITEM_QUIVERS); return; } @@ -331,7 +331,7 @@ void craft_quiver_max() { missing += "2 skins "; if (get_personal_count(ITEM_VINES) < 2) missing += "2 vines "; - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); return; } @@ -361,7 +361,7 @@ void craft_canoe() { if (missing == "") { if (get_personal_count(ITEM_CANOES) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more canoes.", true); + speak_cant_carry_any_more_item(ITEM_CANOES); return; } simulate_crafting(40); @@ -374,13 +374,13 @@ void craft_canoe() { add_personal_count(ITEM_CANOES, 1); speak_with_history("Crafted a Canoe.", true); } else { - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); } } void craft_canoe_max() { if (get_personal_count(ITEM_CANOES) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more canoes.", true); + speak_cant_carry_any_more_item(ITEM_CANOES); return; } @@ -420,7 +420,7 @@ void craft_canoe_max() { missing += "2 rope "; if (get_personal_count(ITEM_REEDS) < 6) missing += "6 reeds "; - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); return; } @@ -444,7 +444,7 @@ void craft_reed_basket() { if (missing == "") { if (get_personal_count(ITEM_REED_BASKETS) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more reed baskets.", true); + speak_cant_carry_any_more_item(ITEM_REED_BASKETS); return; } simulate_crafting(3); @@ -452,13 +452,13 @@ void craft_reed_basket() { add_personal_count(ITEM_REED_BASKETS, 1); speak_with_history("Crafted a reed basket.", true); } else { - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); } } void craft_reed_basket_max() { if (get_personal_count(ITEM_REED_BASKETS) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more reed baskets.", true); + speak_cant_carry_any_more_item(ITEM_REED_BASKETS); return; } @@ -469,7 +469,7 @@ void craft_reed_basket_max() { max_craft = space; if (max_craft <= 0) { - speak_with_history("Missing: 3 reeds", true); + speak_crafting_missing("3 reeds"); return; } @@ -489,13 +489,13 @@ void craft_clay_pot() { // Check for fire within 3 tiles (can hear it) WorldFire @fire = get_fire_within_range(x, 3); if (fire == null) { - speak_with_history("You need a fire within 3 tiles to craft a clay pot.", true); + speak_with_history(tr("system.crafting.require.fire_within_three_clay_pot"), true); return; } if (missing == "") { if (get_personal_count(ITEM_CLAY_POTS) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more clay pots.", true); + speak_cant_carry_any_more_item(ITEM_CLAY_POTS); return; } simulate_crafting(3); @@ -503,7 +503,7 @@ void craft_clay_pot() { add_personal_count(ITEM_CLAY_POTS, 1); speak_with_history("Crafted a clay pot.", true); } else { - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); } } @@ -511,12 +511,12 @@ void craft_clay_pot_max() { // Check for fire within 3 tiles (can hear it) WorldFire @fire = get_fire_within_range(x, 3); if (fire == null) { - speak_with_history("You need a fire within 3 tiles to craft clay pots.", true); + speak_with_history(tr("system.crafting.require.fire_within_three_clay_pots"), true); return; } if (get_personal_count(ITEM_CLAY_POTS) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more clay pots.", true); + speak_cant_carry_any_more_item(ITEM_CLAY_POTS); return; } @@ -527,7 +527,7 @@ void craft_clay_pot_max() { max_craft = space; if (max_craft <= 0) { - speak_with_history("Missing: 3 clay", true); + speak_crafting_missing("3 clay"); return; } diff --git a/src/crafting/craft_weapons.nvgt b/src/crafting/craft_weapons.nvgt index 7feac2c..fc7ddcd 100644 --- a/src/crafting/craft_weapons.nvgt +++ b/src/crafting/craft_weapons.nvgt @@ -1,9 +1,9 @@ // Crafting weapons void run_weapons_menu() { int selection = 0; - string[] options = {"Spear (1 Stick, 1 Vine, 1 Stone) [Requires Knife]", "Sling (1 Skin, 2 Vines)", - "Bow (1 Stick, 1 Bowstring)"}; - speak_with_history("Weapons. " + options[selection], true); + string[] options = {tr("system.crafting.weapons.option.spear"), tr("system.crafting.weapons.option.sling"), + tr("system.crafting.weapons.option.bow")}; + speak_menu_prompt("system.crafting.weapons.prompt", options[selection]); while (true) { wait(5); @@ -68,7 +68,7 @@ void craft_spear() { if (missing == "") { if (get_personal_count(ITEM_SPEARS) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more spears.", true); + speak_cant_carry_any_more_item(ITEM_SPEARS); return; } simulate_crafting(3); @@ -78,17 +78,17 @@ void craft_spear() { add_personal_count(ITEM_SPEARS, 1); speak_with_history("Crafted a Spear.", true); } else { - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); } } void craft_spear_max() { if (get_personal_count(ITEM_KNIVES) < 1) { - speak_with_history("Missing: Stone Knife", true); + speak_crafting_missing("Stone Knife"); return; } if (get_personal_count(ITEM_SPEARS) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more spears.", true); + speak_cant_carry_any_more_item(ITEM_SPEARS); return; } @@ -113,7 +113,7 @@ void craft_spear_max() { missing += "1 vine "; if (get_personal_count(ITEM_STONES) < 1) missing += "1 stone "; - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); return; } @@ -136,7 +136,7 @@ void craft_sling() { if (missing == "") { if (get_personal_count(ITEM_SLINGS) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more slings.", true); + speak_cant_carry_any_more_item(ITEM_SLINGS); return; } simulate_crafting(3); @@ -145,13 +145,13 @@ void craft_sling() { add_personal_count(ITEM_SLINGS, 1); speak_with_history("Crafted a Sling.", true); } else { - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); } } void craft_sling_max() { if (get_personal_count(ITEM_SLINGS) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more slings.", true); + speak_cant_carry_any_more_item(ITEM_SLINGS); return; } @@ -171,7 +171,7 @@ void craft_sling_max() { missing += "1 skin "; if (get_personal_count(ITEM_VINES) < 2) missing += "2 vines "; - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); return; } @@ -193,7 +193,7 @@ void craft_bow() { if (missing == "") { if (get_personal_count(ITEM_BOWS) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more bows.", true); + speak_cant_carry_any_more_item(ITEM_BOWS); return; } simulate_crafting(3); @@ -202,13 +202,13 @@ void craft_bow() { add_personal_count(ITEM_BOWS, 1); speak_with_history("Crafted a Bow.", true); } else { - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); } } void craft_bow_max() { if (get_personal_count(ITEM_BOWS) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more bows.", true); + speak_cant_carry_any_more_item(ITEM_BOWS); return; } @@ -228,7 +228,7 @@ void craft_bow_max() { missing += "1 stick "; if (get_personal_count(ITEM_BOWSTRINGS) < 1) missing += "1 bowstring "; - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); return; } @@ -254,7 +254,7 @@ void craft_axe() { if (missing == "") { if (get_personal_count(ITEM_AXES) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more stone axes.", true); + speak_cant_carry_any_more_item(ITEM_AXES); return; } simulate_crafting(4); @@ -264,17 +264,17 @@ void craft_axe() { add_personal_count(ITEM_AXES, 1); speak_with_history("Crafted a Stone Axe.", true); } else { - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); } } void craft_axe_max() { if (get_personal_count(ITEM_KNIVES) < 1) { - speak_with_history("Missing: Stone Knife", true); + speak_crafting_missing("Stone Knife"); return; } if (get_personal_count(ITEM_AXES) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more stone axes.", true); + speak_cant_carry_any_more_item(ITEM_AXES); return; } @@ -299,7 +299,7 @@ void craft_axe_max() { missing += "1 vine "; if (get_personal_count(ITEM_STONES) < 2) missing += "2 stones "; - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); return; } diff --git a/src/crafting/crafting_core.nvgt b/src/crafting/crafting_core.nvgt index 2f7ffe6..616b97a 100644 --- a/src/crafting/crafting_core.nvgt +++ b/src/crafting/crafting_core.nvgt @@ -1,4 +1,123 @@ // Crafting core menu and shared helpers +void speak_menu_prompt(const string& in promptKey, const string& in option) { + dictionary args; + args.set("option", option); + speak_with_history(trf(promptKey, args), true); +} + +bool crafting_is_numeric_token(const string& in token) { + if (token == "") + return false; + for (uint i = 0; i < token.length(); i++) { + string ch = token.substr(i, 1); + if (ch < "0" || ch > "9") + return false; + } + return true; +} + +string crafting_translate_requirement_label(const string& in englishLabel) { + string trimmed = i18n_trim_whitespace(englishLabel); + if (trimmed == "") + return ""; + + string lower = trimmed.lower(); + if (lower == "stone knife") + return tr("system.crafting.requirement.stone_knife"); + if (lower == "game") + return tr("system.crafting.requirement.game"); + if (lower == "favor") + return tr("system.crafting.requirement.favor"); + if (lower == "resources") + return tr("system.crafting.requirement.resources"); + + string localized = i18n_translate_fragment_value(trimmed); + if (localized != trimmed) + return localized; + + string localizedLower = i18n_translate_fragment_value(lower); + if (localizedLower != lower) + return localizedLower; + + return trimmed; +} + +void crafting_append_requirement_fragment(string &inout joined, const string& in fragment) { + string trimmed = i18n_trim_whitespace(fragment); + if (trimmed == "") + return; + if (joined != "") + joined += ", "; + joined += trimmed; +} + +string crafting_localize_missing_requirements(const string& in missingEnglish) { + string trimmed = i18n_trim_whitespace(missingEnglish); + if (trimmed == "") + return ""; + + string[] tokens = trimmed.split(" "); + string localized = ""; + uint i = 0; + + while (i < tokens.length()) { + if (tokens[i] == "") { + i++; + continue; + } + + if (crafting_is_numeric_token(tokens[i])) { + string countText = tokens[i]; + i++; + + string label = ""; + while (i < tokens.length()) { + if (tokens[i] == "") { + i++; + continue; + } + if (crafting_is_numeric_token(tokens[i])) + break; + if (label != "") + label += " "; + label += tokens[i]; + i++; + } + + label = crafting_translate_requirement_label(label); + crafting_append_requirement_fragment(localized, countText + " " + label); + continue; + } + + string label = ""; + while (i < tokens.length()) { + if (tokens[i] == "") { + i++; + continue; + } + if (crafting_is_numeric_token(tokens[i])) + break; + if (label != "") + label += " "; + label += tokens[i]; + i++; + } + + label = crafting_translate_requirement_label(label); + crafting_append_requirement_fragment(localized, label); + } + + if (localized == "") + localized = crafting_translate_requirement_label(trimmed); + return localized; +} + +void speak_crafting_missing(const string& in missingEnglish) { + dictionary args; + args.set("requirements", crafting_localize_missing_requirements(missingEnglish)); + speak_with_history(trf("system.crafting.missing", args), true); +} + void check_crafting_menu(int x, int base_end_tile) { if (x <= base_end_tile) { if (key_pressed(KEY_C)) { @@ -13,27 +132,27 @@ void run_crafting_menu() { int[] category_types; // Build categories dynamically - categories.insert_last("Weapons"); + categories.insert_last(tr("system.crafting.category.weapons")); category_types.insert_last(0); - categories.insert_last("Tools"); + categories.insert_last(tr("system.crafting.category.tools")); category_types.insert_last(1); - categories.insert_last("Materials"); + categories.insert_last(tr("system.crafting.category.materials")); category_types.insert_last(2); - categories.insert_last("Clothing"); + categories.insert_last(tr("system.crafting.category.clothing")); category_types.insert_last(3); if (has_building_options()) { - categories.insert_last("Buildings"); + categories.insert_last(tr("system.crafting.category.buildings")); category_types.insert_last(4); } - categories.insert_last("Barricade"); + categories.insert_last(tr("system.crafting.category.barricade")); category_types.insert_last(5); // Add Runes category if any rune is unlocked if (any_rune_unlocked()) { - categories.insert_last("Runes"); + categories.insert_last(tr("system.crafting.category.runes")); category_types.insert_last(6); } - speak_with_history("Crafting menu. " + categories[selection], true); + speak_menu_prompt("system.crafting.menu.prompt", categories[selection]); while (true) { wait(5); diff --git a/src/environment.nvgt b/src/environment.nvgt index 925df4e..468fd6b 100644 --- a/src/environment.nvgt +++ b/src/environment.nvgt @@ -218,19 +218,19 @@ void init_search_pools() { @search_pools[SEARCH_POOL_STREAM_BANK] = SearchPool(); search_pools[SEARCH_POOL_STREAM_BANK].item_types = {ITEM_REEDS, ITEM_CLAY}; search_pools[SEARCH_POOL_STREAM_BANK].weights = {30, 70}; - search_pools[SEARCH_POOL_STREAM_BANK].found_messages = {"Found a reed.", "Found clay."}; + search_pools[SEARCH_POOL_STREAM_BANK].found_messages = {"", ""}; search_pools[SEARCH_POOL_STREAM_BANK].terrain_tags = {"stream_bank"}; @search_pools[SEARCH_POOL_FOREST] = SearchPool(); search_pools[SEARCH_POOL_FOREST].item_types = {ITEM_STICKS, ITEM_VINES}; search_pools[SEARCH_POOL_FOREST].weights = {1, 1}; - search_pools[SEARCH_POOL_FOREST].found_messages = {"Found a stick.", "Found a vine."}; + search_pools[SEARCH_POOL_FOREST].found_messages = {"", ""}; search_pools[SEARCH_POOL_FOREST].terrain_tags = {"forest", "deep_forest"}; @search_pools[SEARCH_POOL_STONE_TERRAIN] = SearchPool(); search_pools[SEARCH_POOL_STONE_TERRAIN].item_types = {ITEM_STONES}; search_pools[SEARCH_POOL_STONE_TERRAIN].weights = {1}; - search_pools[SEARCH_POOL_STONE_TERRAIN].found_messages = {"Found a stone."}; + search_pools[SEARCH_POOL_STONE_TERRAIN].found_messages = {""}; search_pools[SEARCH_POOL_STONE_TERRAIN].terrain_tags = {"gravel", "stone", "hard_stone"}; // Mass nouns for auto "Found X." fallback (no article). @@ -596,14 +596,19 @@ void damage_tree(int target_x, int damage) { add_personal_count(ITEM_VINES, vines_added); add_personal_count(ITEM_LOGS, logs_added); - string drop_message = "Tree fell!"; + string drop_message = tr("system.environment.tree.fell"); if (sticks_added > 0 || vines_added > 0 || logs_added > 0) { - string log_label = (logs_added == 1) ? " log" : " logs"; - drop_message += " Got " + sticks_added + " sticks, " + vines_added + " vines, and " + logs_added + - log_label + "."; + dictionary loot_args; + loot_args.set("sticks", sticks_added); + loot_args.set("vines", vines_added); + loot_args.set("logs", logs_added); + loot_args.set("sticks_label", get_item_label_for_count(ITEM_STICKS, sticks_added)); + loot_args.set("vines_label", get_item_label_for_count(ITEM_VINES, vines_added)); + loot_args.set("logs_label", get_item_label_for_count(ITEM_LOGS, logs_added)); + drop_message = trf("system.environment.tree.fell_with_loot", loot_args); } if (sticks_added < sticks_dropped || vines_added < vines_dropped || logs_added < 1) { - drop_message += " Inventory full."; + drop_message += " " + tr("system.environment.tree.inventory_full"); } play_item_collect_sound("stick"); @@ -616,7 +621,7 @@ void damage_tree(int target_x, int damage) { string build_item_list(int[] item_types) { string list_text = ""; for (uint i = 0; i < item_types.length(); i++) { - string item_name = item_registry[item_types[i]].name; + string item_name = get_item_label(item_types[i]); if (i == 0) { list_text = item_name; } else if (i == item_types.length() - 1) { @@ -642,17 +647,9 @@ bool is_mass_noun_item(int item_type) { } string get_auto_found_message(int item_type) { - string singular = item_registry[item_type].singular; - if (is_mass_noun_item(item_type)) { - return "Found " + singular + "."; - } - - string first_letter = singular.substr(0, 1); - if (first_letter == "a" || first_letter == "e" || first_letter == "i" || first_letter == "o" || - first_letter == "u") { - return "Found an " + singular + "."; - } - return "Found a " + singular + "."; + dictionary args; + args.set("item", get_item_label_singular(item_type)); + return trf("system.search.found_item", args); } int get_search_weight(int[] weights, uint index) { @@ -685,7 +682,7 @@ bool try_find_weighted_resource(int[] item_types, int[] weights, string[] found_ } if (available_indices.length() == 0) { - speak_with_history("You can't carry any more " + build_item_list(item_types) + ".", true); + speak_cant_carry_any_more_label(build_item_list(item_types)); return true; } @@ -758,11 +755,11 @@ void perform_search(int current_x) { if (s != null) { if (s.has_catch) { if (get_personal_count(ITEM_SMALL_GAME) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more small game.", true); + speak_cant_carry_any_more_item(ITEM_SMALL_GAME); return; } if (get_personal_count(ITEM_SNARES) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more snares.", true); + speak_cant_carry_any_more_item(ITEM_SNARES); return; } add_personal_count(ITEM_SMALL_GAME, 1); @@ -771,7 +768,7 @@ void perform_search(int current_x) { speak_with_history("Collected " + s.catch_type + " and snare.", true); } else { if (get_personal_count(ITEM_SNARES) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more snares.", true); + speak_cant_carry_any_more_item(ITEM_SNARES); return; } add_personal_count(ITEM_SNARES, 1); // Recover snare @@ -785,7 +782,7 @@ void perform_search(int current_x) { } if (random(1, 100) <= 10) { - speak_with_history("Found nothing.", true); + speak_with_history(tr("system.search.found_nothing"), true); return; } @@ -865,13 +862,13 @@ void perform_search(int current_x) { nearest.sticks--; add_personal_count(ITEM_STICKS, 1); play_item_collect_sound("stick"); - speak_with_history("Found a stick.", true); + speak_with_history(get_auto_found_message(ITEM_STICKS), true); took_item = true; } else if (nearest.vines > 0 && get_personal_count(ITEM_VINES) < get_personal_stack_limit()) { nearest.vines--; add_personal_count(ITEM_VINES, 1); play_item_collect_sound("vine"); - speak_with_history("Found a vine.", true); + speak_with_history(get_auto_found_message(ITEM_VINES), true); took_item = true; } } else { @@ -879,24 +876,24 @@ void perform_search(int current_x) { nearest.vines--; add_personal_count(ITEM_VINES, 1); play_item_collect_sound("vine"); - speak_with_history("Found a vine.", true); + speak_with_history(get_auto_found_message(ITEM_VINES), true); took_item = true; } else if (nearest.sticks > 0 && get_personal_count(ITEM_STICKS) < get_personal_stack_limit()) { nearest.sticks--; add_personal_count(ITEM_STICKS, 1); play_item_collect_sound("stick"); - speak_with_history("Found a stick.", true); + speak_with_history(get_auto_found_message(ITEM_STICKS), true); took_item = true; } } if (!took_item) { if (nearest.sticks > 0 && nearest.vines > 0) { - speak_with_history("You can't carry any more sticks or vines.", true); + speak_cant_carry_any_more_label(build_item_list({ITEM_STICKS, ITEM_VINES})); } else if (nearest.sticks > 0) { - speak_with_history("You can't carry any more sticks.", true); + speak_cant_carry_any_more_item(ITEM_STICKS); } else { - speak_with_history("You can't carry any more vines.", true); + speak_cant_carry_any_more_item(ITEM_VINES); } return; } @@ -916,7 +913,7 @@ void perform_search(int current_x) { return; } - speak_with_history("Found nothing.", true); + speak_with_history(tr("system.search.found_nothing"), true); } // Climbing functions diff --git a/src/fishing.nvgt b/src/fishing.nvgt index c90d299..b5bf7a9 100644 --- a/src/fishing.nvgt +++ b/src/fishing.nvgt @@ -310,7 +310,7 @@ void update_reeling() { void catch_fish() { if (get_personal_count(ITEM_FISH) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more fish.", true); + speak_cant_carry_any_more_item(ITEM_FISH); reset_fishing_session(); return; } diff --git a/src/fylgja_system.nvgt b/src/fylgja_system.nvgt index d401f26..2999770 100644 --- a/src/fylgja_system.nvgt +++ b/src/fylgja_system.nvgt @@ -88,7 +88,7 @@ void append_adventure_completion_rewards(int adventureId, string[] @rewards) { string stageName = fylgjaStageNames[stageIndex]; string targetName = adventureStageTargets[adventureIndex]; - rewards.insert_last("You have a " + stageName + " connection with the " + targetName + "."); + rewards.insert_last(i18n_text("You have a " + stageName + " connection with the " + targetName + ".")); int fylgjaIndex = get_fylgja_index_for_adventure(adventureId); if (fylgjaIndex == -1) @@ -96,9 +96,9 @@ void append_adventure_completion_rewards(int adventureId, string[] @rewards) { if (completionCount >= FYLGJA_STAGE_COUNT) { if (completionCount == FYLGJA_STAGE_COUNT) { - rewards.insert_last("You have unlocked the " + fylgjaNames[fylgjaIndex] + " Fylgja!"); + rewards.insert_last(i18n_text("You have unlocked the " + fylgjaNames[fylgjaIndex] + " Fylgja!")); } else { - rewards.insert_last("You have already unlocked the " + fylgjaNames[fylgjaIndex] + " Fylgja."); + rewards.insert_last(i18n_text("You have already unlocked the " + fylgjaNames[fylgjaIndex] + " Fylgja.")); } } } @@ -129,6 +129,7 @@ void run_fylgja_menu() { fylgjaIndices.insert_last(int(i)); } } + i18n_translate_string_array_in_place(options); if (options.length() == 0) return; diff --git a/src/i18n.nvgt b/src/i18n.nvgt new file mode 100644 index 0000000..3252e25 --- /dev/null +++ b/src/i18n.nvgt @@ -0,0 +1,770 @@ +// Localization and translation helpers. +// Provides key-based lookups plus runtime template matching for existing English output. + +const string I18N_DEFAULT_LANGUAGE_CODE = "en"; +const string I18N_LANGUAGE_DIRECTORY = "lang"; +const string I18N_ENGLISH_CATALOG_PATH = "lang/en.ini"; +const string I18N_LANGUAGE_PREF_FILENAME = "draugnorak.dat"; +const string I18N_LANGUAGE_PREF_KEY = "language"; +const string I18N_PREF_COMPANY_NAME = "stormux"; +const string I18N_PREF_PRODUCT_NAME = "Draugnorak"; +const string I18N_PREF_SUBDIR = "config"; + +string activeLanguageCode = I18N_DEFAULT_LANGUAGE_CODE; +bool i18nInitialized = false; +bool i18nCatalogLoaded = false; + +dictionary @i18nEnglishCatalog = dictionary(); +dictionary @i18nActiveCatalog = dictionary(); +dictionary @i18nExactEnglishToKey = dictionary(); +dictionary @i18nMissingKeyLog = dictionary(); + +string[] i18nTemplateKeys; +string[] i18nTemplateEnglish; + +string i18n_trim_whitespace(string text) { + if (text.length() == 0) + return text; + + int start = 0; + int end = text.length() - 1; + while (start <= end) { + string ch = text.substr(start, 1); + if (ch != " " && ch != "\t" && ch != "\r" && ch != "\n") + break; + start++; + } + + while (end >= start) { + string ch = text.substr(end, 1); + if (ch != " " && ch != "\t" && ch != "\r" && ch != "\n") + break; + end--; + } + + if (end < start) + return ""; + return text.substr(start, end - start + 1); +} + +string i18n_trim_trailing_path_separator(string path) { + while (path.length() > 1) { + string last = path.substr(path.length() - 1, 1); + if (last != "/" && last != "\\") + break; + path = path.substr(0, path.length() - 1); + } + return path; +} + +string i18n_get_path_basename(const string& in path) { + int lastSeparator = -1; + for (uint i = 0; i < path.length(); i++) { + string ch = path.substr(i, 1); + if (ch == "/" || ch == "\\") { + lastSeparator = int(i); + } + } + + if (lastSeparator < 0) + return path; + int start = lastSeparator + 1; + if (start >= int(path.length())) + return ""; + return path.substr(start); +} + +string i18n_get_pref_directory_path() { + string path = join({DIRECTORY_APPDATA, I18N_PREF_COMPANY_NAME, I18N_PREF_PRODUCT_NAME, I18N_PREF_SUBDIR}, "/"); + path = i18n_trim_trailing_path_separator(path); + if (path == "") + path = "."; + + if (!directory_exists(path) && !directory_create(path)) { + return "."; + } + + return path; +} + +string i18n_get_pref_file_path() { + string prefDirectory = i18n_get_pref_directory_path(); + if (prefDirectory == "." || prefDirectory == "") { + return I18N_LANGUAGE_PREF_FILENAME; + } + return prefDirectory + "/" + I18N_LANGUAGE_PREF_FILENAME; +} + +bool i18n_read_file_text(const string& in filePath, string& out content) { + content = ""; + file handle; + if (!handle.open(filePath, "rb")) + return false; + content = handle.read(); + handle.close(); + return true; +} + +bool i18n_write_file_text(const string& in filePath, const string& in content) { + file handle; + if (!handle.open(filePath, "wb")) + return false; + int written = handle.write(content); + handle.close(); + return written >= int(content.length()); +} + +string i18n_unescape_catalog_value(const string& in value) { + string result = ""; + bool escaping = false; + + for (uint i = 0; i < value.length(); i++) { + string ch = value.substr(i, 1); + if (!escaping) { + if (ch == "\\") { + escaping = true; + continue; + } + result += ch; + continue; + } + + if (ch == "n") { + result += "\n"; + } else if (ch == "r") { + result += "\r"; + } else if (ch == "t") { + result += "\t"; + } else { + result += ch; + } + escaping = false; + } + + if (escaping) + result += "\\"; + + return result; +} + +bool i18n_parse_catalog_line(const string& in lineText, string& out key, string& out value) { + key = ""; + value = ""; + + string trimmed = i18n_trim_whitespace(lineText); + if (trimmed == "") + return false; + if (trimmed.substr(0, 1) == ";" || trimmed.substr(0, 1) == "#") + return false; + + int separator = trimmed.find_first("="); + if (separator < 0) + return false; + + string rawKey = i18n_trim_whitespace(trimmed.substr(0, separator)); + string rawValue = ""; + if (separator + 1 < int(trimmed.length())) { + rawValue = i18n_trim_whitespace(trimmed.substr(separator + 1)); + } + + if (rawKey == "") + return false; + + key = rawKey; + value = i18n_unescape_catalog_value(rawValue); + return true; +} + +bool i18n_load_catalog_file(const string& in filePath, dictionary @catalog) { + if (@catalog is null) + return false; + + catalog.clear(); + string data = ""; + if (!i18n_read_file_text(filePath, data)) + return false; + + string[] lines = data.split("\n"); + string currentSection = ""; + for (uint i = 0; i < lines.length(); i++) { + string line = lines[i]; + if (line.length() > 0 && line.substr(line.length() - 1) == "\r") { + line = line.substr(0, line.length() - 1); + } + + string trimmed = i18n_trim_whitespace(line); + if (trimmed.length() >= 2 && trimmed.substr(0, 1) == "[" && trimmed.substr(trimmed.length() - 1) == "]") { + currentSection = i18n_trim_whitespace(trimmed.substr(1, trimmed.length() - 2)); + continue; + } + + string key = ""; + string value = ""; + if (!i18n_parse_catalog_line(trimmed, key, value)) + continue; + + if (currentSection != "") { + if (currentSection == "messages") { + // Keep message keys stable as msg. so runtime template matching can resolve them directly. + if (key.find_first(".") < 0) + key = currentSection + "." + key; + } else { + string sectionPrefix = currentSection + "."; + if (key.find_first(sectionPrefix) != 0) + key = sectionPrefix + key; + } + } + + catalog.set(key, value); + } + + return catalog.get_size() > 0; +} + +bool i18n_catalog_get_string(dictionary @catalog, const string& in key, string& out value) { + value = ""; + if (@catalog is null) + return false; + if (catalog.get(key, value)) + return true; + + int valueInt = 0; + if (catalog.get(key, valueInt)) { + value = "" + valueInt; + return true; + } + + double valueDouble = 0.0; + if (catalog.get(key, valueDouble)) { + value = "" + valueDouble; + return true; + } + + bool valueBool = false; + if (catalog.get(key, valueBool)) { + value = valueBool ? "true" : "false"; + return true; + } + + return false; +} + +bool i18n_has_placeholders(const string& in templateText) { + int open = templateText.find_first("{"); + if (open < 0) + return false; + int close = templateText.find_first("}", open + 1); + return close > open; +} + +int i18n_template_literal_length(const string& in templateText) { + int literalCount = 0; + bool inPlaceholder = false; + + for (uint i = 0; i < templateText.length(); i++) { + string ch = templateText.substr(i, 1); + if (!inPlaceholder && ch == "{") { + inPlaceholder = true; + continue; + } + if (inPlaceholder && ch == "}") { + inPlaceholder = false; + continue; + } + if (!inPlaceholder) { + literalCount++; + } + } + + return literalCount; +} + +string i18n_lookup_key_with_fallback(const string& in key, const string& in defaultValue) { + string localized = ""; + if (@i18nActiveCatalog !is null && i18n_catalog_get_string(i18nActiveCatalog, key, localized) && localized != "") { + return localized; + } + + if (@i18nEnglishCatalog !is null && i18n_catalog_get_string(i18nEnglishCatalog, key, localized) && localized != "") { + return localized; + } + + return defaultValue; +} + +void i18n_rebuild_indexes() { + i18nExactEnglishToKey.clear(); + i18nTemplateKeys.resize(0); + i18nTemplateEnglish.resize(0); + + if (@i18nEnglishCatalog is null) + return; + + string[] @keys = i18nEnglishCatalog.get_keys(); + if (@keys is null) + return; + + for (uint i = 0; i < keys.length(); i++) { + string key = keys[i]; + if (key.find_first("meta.") == 0) + continue; + + string english = ""; + if (!i18n_catalog_get_string(i18nEnglishCatalog, key, english)) + continue; + if (english == "") + continue; + + if (i18n_has_placeholders(english)) { + if (i18n_template_literal_length(english) == 0) + continue; + i18nTemplateKeys.insert_last(key); + i18nTemplateEnglish.insert_last(english); + } else if (!i18nExactEnglishToKey.exists(english)) { + i18nExactEnglishToKey.set(english, key); + } + } +} + +string tr(const string& in key) { + if (key == "") + return ""; + + string value = ""; + if (@i18nActiveCatalog !is null && i18n_catalog_get_string(i18nActiveCatalog, key, value) && value != "") + return value; + if (@i18nEnglishCatalog !is null && i18n_catalog_get_string(i18nEnglishCatalog, key, value) && value != "") + return value; + + if (!i18nMissingKeyLog.exists(key)) { + i18nMissingKeyLog.set(key, true); + } + + return key; +} + +string i18n_format_template(const string& in templateText, dictionary @args) { + string result = ""; + uint cursor = 0; + + while (cursor < templateText.length()) { + int open = templateText.find_first("{", cursor); + if (open < 0) { + result += templateText.substr(cursor); + break; + } + + result += templateText.substr(cursor, open - cursor); + int close = templateText.find_first("}", open + 1); + if (close < 0) { + result += templateText.substr(open); + break; + } + + string placeholder = templateText.substr(open + 1, close - open - 1); + string replacement = ""; + if (!i18n_catalog_get_string(args, placeholder, replacement)) { + replacement = "{" + placeholder + "}"; + } + result += replacement; + cursor = close + 1; + } + + return result; +} + +string trf(const string& in key, dictionary @args) { + string templateText = tr(key); + return i18n_format_template(templateText, args); +} + +string trn(const string& in baseKey, int count, dictionary @args = null) { + string key = baseKey + ((count == 1) ? ".one" : ".other"); + + dictionary localArgs; + dictionary @argsHandle = @args; + if (@argsHandle is null) { + @argsHandle = @localArgs; + } + argsHandle.set("count", count); + + return trf(key, argsHandle); +} + +bool i18n_match_template(const string& in templateText, const string& in inputText, dictionary @captures) { + if (@captures is null) + return false; + captures.clear(); + + uint templateCursor = 0; + uint inputCursor = 0; + + while (templateCursor < templateText.length()) { + int open = templateText.find_first("{", templateCursor); + if (open < 0) { + string tail = templateText.substr(templateCursor); + uint remaining = inputText.length() - inputCursor; + if (tail.length() != remaining) + return false; + if (tail.length() > 0 && inputText.substr(inputCursor, tail.length()) != tail) + return false; + inputCursor = inputText.length(); + templateCursor = templateText.length(); + break; + } + + string literalBefore = templateText.substr(templateCursor, open - templateCursor); + if (literalBefore.length() > 0) { + if (inputCursor + literalBefore.length() > inputText.length()) + return false; + if (inputText.substr(inputCursor, literalBefore.length()) != literalBefore) + return false; + inputCursor += literalBefore.length(); + } + + int close = templateText.find_first("}", open + 1); + if (close < 0) + return false; + + string placeholder = templateText.substr(open + 1, close - open - 1); + if (placeholder == "") + return false; + + int nextOpen = templateText.find_first("{", close + 1); + string nextLiteral = ""; + if (nextOpen >= 0) { + nextLiteral = templateText.substr(close + 1, nextOpen - close - 1); + } else { + nextLiteral = templateText.substr(close + 1); + } + + string captured = ""; + if (nextLiteral == "") { + if (nextOpen >= 0) + return false; + if (inputCursor <= inputText.length()) { + captured = inputText.substr(inputCursor); + } + inputCursor = inputText.length(); + } else { + int found = inputText.find_first(nextLiteral, inputCursor); + if (found < 0) + return false; + captured = inputText.substr(inputCursor, found - inputCursor); + inputCursor = found; + } + + captures.set(placeholder, captured); + templateCursor = close + 1; + } + + return inputCursor == inputText.length(); +} + +string i18n_translate_fragment_value(const string& in value) { + if (value == "" || activeLanguageCode == I18N_DEFAULT_LANGUAGE_CODE) + return value; + + string key = ""; + if (!i18n_catalog_get_string(i18nExactEnglishToKey, value, key)) + return value; + + return i18n_lookup_key_with_fallback(key, value); +} + +string i18n_translate_speech_message(const string& in message) { + if (message == "") + return message; + if (!i18nCatalogLoaded) + return message; + if (activeLanguageCode == I18N_DEFAULT_LANGUAGE_CODE) + return message; + + string key = ""; + if (i18n_catalog_get_string(i18nExactEnglishToKey, message, key)) { + return i18n_lookup_key_with_fallback(key, message); + } + + for (uint i = 0; i < i18nTemplateEnglish.length(); i++) { + dictionary captures; + if (!i18n_match_template(i18nTemplateEnglish[i], message, captures)) + continue; + + string[] @captureKeys = captures.get_keys(); + if (@captureKeys !is null) { + for (uint keyIndex = 0; keyIndex < captureKeys.length(); keyIndex++) { + string captureKey = captureKeys[keyIndex]; + string captureValue = ""; + if (!i18n_catalog_get_string(captures, captureKey, captureValue)) + continue; + captures.set(captureKey, i18n_translate_fragment_value(captureValue)); + } + } + + string localizedTemplate = i18n_lookup_key_with_fallback(i18nTemplateKeys[i], i18nTemplateEnglish[i]); + return i18n_format_template(localizedTemplate, captures); + } + + return message; +} + +string i18n_text(const string& in message) { + return i18n_translate_speech_message(message); +} + +void i18n_translate_string_array_in_place(string[] &inout values) { + for (uint i = 0; i < values.length(); i++) { + values[i] = i18n_translate_speech_message(values[i]); + } +} + +bool i18n_load_language(const string& in languageCode) { + if (@i18nActiveCatalog is null) + @i18nActiveCatalog = dictionary(); + + string normalized = i18n_trim_whitespace(languageCode); + if (normalized == "") + normalized = I18N_DEFAULT_LANGUAGE_CODE; + + if (normalized == I18N_DEFAULT_LANGUAGE_CODE) { + i18nActiveCatalog.clear(); + activeLanguageCode = I18N_DEFAULT_LANGUAGE_CODE; + i18nCatalogLoaded = true; + return true; + } + + string catalogPath = I18N_LANGUAGE_DIRECTORY + "/" + normalized + ".ini"; + dictionary loadedCatalog; + if (!i18n_load_catalog_file(catalogPath, loadedCatalog)) { + return false; + } + + i18nActiveCatalog.clear(); + string[] @loadedKeys = loadedCatalog.get_keys(); + if (@loadedKeys !is null) { + for (uint i = 0; i < loadedKeys.length(); i++) { + string key = loadedKeys[i]; + string value = ""; + if (!i18n_catalog_get_string(loadedCatalog, key, value)) + continue; + i18nActiveCatalog.set(key, value); + } + } + + activeLanguageCode = normalized; + i18nCatalogLoaded = true; + return true; +} + +void i18n_save_language_preference(const string& in languageCode) { + string code = i18n_trim_whitespace(languageCode); + if (code == "") + code = I18N_DEFAULT_LANGUAGE_CODE; + + string content = I18N_LANGUAGE_PREF_KEY + "=" + code + "\n"; + i18n_write_file_text(i18n_get_pref_file_path(), content); +} + +string i18n_load_language_preference() { + string data = ""; + if (!i18n_read_file_text(i18n_get_pref_file_path(), data)) + return ""; + + string[] lines = data.split("\n"); + for (uint i = 0; i < lines.length(); i++) { + string line = lines[i]; + if (line.length() > 0 && line.substr(line.length() - 1) == "\r") + line = line.substr(0, line.length() - 1); + + string key = ""; + string value = ""; + if (!i18n_parse_catalog_line(line, key, value)) + continue; + if (key == I18N_LANGUAGE_PREF_KEY) + return i18n_trim_whitespace(value); + } + + return ""; +} + +void i18n_get_available_languages(string[] &out languageCodes, string[] &out languageLabels) { + languageCodes.resize(0); + languageLabels.resize(0); + + string[] @paths = glob(I18N_LANGUAGE_DIRECTORY + "/*.ini"); + if (@paths is null) + return; + + bool hasEnglish = false; + + for (uint i = 0; i < paths.length(); i++) { + string fileName = i18n_get_path_basename(paths[i]); + string lowerFileName = fileName.lower(); + if (lowerFileName.length() >= 13 && lowerFileName.substr(lowerFileName.length() - 13) == ".template.ini") + continue; + if (fileName.length() < 5) + continue; + if (fileName.substr(fileName.length() - 4).lower() != ".ini") + continue; + + string code = fileName.substr(0, fileName.length() - 4); + if (code == "") + continue; + + if (code.lower() == I18N_DEFAULT_LANGUAGE_CODE) + hasEnglish = true; + + dictionary tempCatalog; + string label = code; + if (i18n_load_catalog_file(paths[i], tempCatalog)) { + string nativeName = ""; + if (i18n_catalog_get_string(tempCatalog, "meta.native_name", nativeName) && nativeName != "") { + label = nativeName + " (" + code + ")"; + } else if (i18n_catalog_get_string(tempCatalog, "meta.name", nativeName) && nativeName != "") { + label = nativeName + " (" + code + ")"; + } + } + + languageCodes.insert_last(code); + languageLabels.insert_last(label); + } + + if (!hasEnglish) { + languageCodes.insert_last(I18N_DEFAULT_LANGUAGE_CODE); + languageLabels.insert_last(i18n_lookup_key_with_fallback("system.language.english_label", "English (en)")); + } +} + +int i18n_find_language_index(const string[] @languageCodes, const string& in languageCode) { + if (@languageCodes is null || languageCodes.length() == 0) + return -1; + + string target = i18n_trim_whitespace(languageCode).lower(); + if (target == "") + return -1; + + for (uint i = 0; i < languageCodes.length(); i++) { + if (languageCodes[i].lower() == target) + return i; + } + return -1; +} + +int i18n_run_language_selection_menu(const string[] @languageCodes, const string[] @languageLabels, + int defaultIndex = 0) { + if (@languageCodes is null || @languageLabels is null) + return -1; + if (languageCodes.length() == 0 || languageLabels.length() == 0) + return -1; + + int selection = defaultIndex; + if (selection < 0 || selection >= int(languageLabels.length())) + selection = 0; + + string prompt = tr("system.language.select_prompt"); + if (prompt.length() > 0 && prompt.substr(prompt.length() - 1) == ".") { + prompt = prompt.substr(0, prompt.length() - 1); + } + if (prompt != "") { + speak_with_history(prompt + " " + languageLabels[selection], true); + } else { + speak_with_history(languageLabels[selection], true); + } + + while (true) { + wait(5); + handle_global_volume_keys(); + + if (key_pressed(KEY_DOWN)) { + play_menu_move_sound(); + selection++; + if (selection >= int(languageLabels.length())) + selection = 0; + speak_with_history(languageLabels[selection], true); + } + + if (key_pressed(KEY_UP)) { + play_menu_move_sound(); + selection--; + if (selection < 0) + selection = languageLabels.length() - 1; + speak_with_history(languageLabels[selection], true); + } + + if (key_pressed(KEY_RETURN)) { + play_menu_select_sound(); + return selection; + } + + if (key_pressed(KEY_ESCAPE)) { + exit(); + } + } + + return selection; +} + +bool i18n_load_english_catalog() { + if (@i18nEnglishCatalog is null) + @i18nEnglishCatalog = dictionary(); + + if (!i18n_load_catalog_file(I18N_ENGLISH_CATALOG_PATH, i18nEnglishCatalog)) { + i18nEnglishCatalog.clear(); + i18nEnglishCatalog.set("system.language.select_prompt", "Select your language."); + i18nEnglishCatalog.set("system.language.selected", "Language set to {language}."); + i18nEnglishCatalog.set("system.ui.window_title", "Draugnorak"); + } + + i18n_rebuild_indexes(); + return i18nEnglishCatalog.get_size() > 0; +} + +void i18n_init() { + if (i18nInitialized) + return; + i18nInitialized = true; + + i18n_load_english_catalog(); + speech_history_set_message_transform_callback(i18n_translate_speech_message); + ui_set_text_transform_callback(i18n_translate_speech_message); + + string[] languageCodes; + string[] languageLabels; + i18n_get_available_languages(languageCodes, languageLabels); + + string preferredLanguage = i18n_load_language_preference(); + int preferredIndex = i18n_find_language_index(languageCodes, preferredLanguage); + bool promptedForLanguage = false; + + if (preferredIndex < 0) { + promptedForLanguage = true; + int englishIndex = i18n_find_language_index(languageCodes, I18N_DEFAULT_LANGUAGE_CODE); + if (englishIndex < 0) + englishIndex = 0; + preferredIndex = i18n_run_language_selection_menu(languageCodes, languageLabels, englishIndex); + } + + string chosenLanguage = I18N_DEFAULT_LANGUAGE_CODE; + if (preferredIndex >= 0 && preferredIndex < int(languageCodes.length())) { + chosenLanguage = languageCodes[preferredIndex]; + } + + if (!i18n_load_language(chosenLanguage)) { + i18n_load_language(I18N_DEFAULT_LANGUAGE_CODE); + chosenLanguage = I18N_DEFAULT_LANGUAGE_CODE; + promptedForLanguage = true; + } + + i18n_save_language_preference(chosenLanguage); + + if (promptedForLanguage) { + dictionary args; + string selectedLabel = chosenLanguage; + int selectedLanguageIndex = i18n_find_language_index(languageCodes, chosenLanguage); + if (selectedLanguageIndex >= 0 && selectedLanguageIndex < int(languageLabels.length())) { + selectedLabel = languageLabels[selectedLanguageIndex]; + } + args.set("language", selectedLabel); + speak_with_history(trf("system.language.selected", args), true); + } +} diff --git a/src/inventory_items.nvgt b/src/inventory_items.nvgt index de8d0e4..bc73211 100644 --- a/src/inventory_items.nvgt +++ b/src/inventory_items.nvgt @@ -94,38 +94,14 @@ void clamp_arrows_to_quiver_limit() { set_personal_count(ITEM_ARROWS, maxArrows); if (maxArrows == 0) { - speak_with_history("You need a quiver to carry arrows.", true); + speak_need_quiver_for_arrows(); } else { speak_with_history("You can only carry " + maxArrows + " arrows with your current quivers.", true); } } string get_equipment_name(int equip_type) { - if (equip_type == EQUIP_SPEAR) - return "Spear"; - if (equip_type == EQUIP_AXE) - return "Stone Axe"; - if (equip_type == EQUIP_SLING) - return "Sling"; - if (equip_type == EQUIP_BOW) - return "Bow"; - if (equip_type == EQUIP_HAT) - return "Skin Hat"; - if (equip_type == EQUIP_GLOVES) - return "Skin Gloves"; - if (equip_type == EQUIP_PANTS) - return "Skin Pants"; - if (equip_type == EQUIP_TUNIC) - return "Skin Tunic"; - if (equip_type == EQUIP_MOCCASINS) - return "Moccasins"; - if (equip_type == EQUIP_POUCH) - return "Skin Pouch"; - if (equip_type == EQUIP_BACKPACK) - return "Backpack"; - if (equip_type == EQUIP_FISHING_POLE) - return "Fishing Pole"; - return "Unknown"; + return get_base_equipment_name(equip_type); } bool equipment_available(int equip_type) { @@ -481,7 +457,9 @@ void activate_quick_slot(int slot_index) { int count = get_item_count_for_type(item_type); speak_with_history(get_item_count_label(item_type, count) + ".", true); } else { - speak_with_history("No item bound to slot " + slot_index + ".", true); + dictionary args; + args.set("slot", slot_index); + speak_with_history(trf("system.quick_slot.no_item_bound", args), true); } return; } @@ -556,34 +534,35 @@ string format_favor(double value) { string get_equipped_weapon_name() { if (spear_equipped) - return "Spear"; + return get_base_equipment_name(EQUIP_SPEAR); if (axe_equipped) - return "Stone Axe"; + return get_base_equipment_name(EQUIP_AXE); if (sling_equipped) - return "Sling"; + return get_base_equipment_name(EQUIP_SLING); if (bow_equipped) - return "Bow"; + return get_base_equipment_name(EQUIP_BOW); if (fishing_pole_equipped) - return "Fishing Pole"; - return "None"; + return get_base_equipment_name(EQUIP_FISHING_POLE); + return i18n_lookup_key_with_fallback("system.equipment.name.none", "None"); } string get_speed_status() { - string status = ""; int rune_bonus = get_total_rune_walk_speed_bonus(); if (blessing_speed_active) { - status = "blessed"; - } else if (equipped_feet == EQUIP_MOCCASINS && rune_bonus > 0) { - status = "boosted by moccasins and runes"; - } else if (equipped_feet == EQUIP_MOCCASINS) { - status = "boosted by moccasins"; - } else if (rune_bonus > 0) { - status = "boosted by runes"; - } else { - status = "normal"; + return i18n_lookup_key_with_fallback("system.character.speed.blessed", "blessed"); } - return status; + if (equipped_feet == EQUIP_MOCCASINS && rune_bonus > 0) { + return i18n_lookup_key_with_fallback( + "system.character.speed.boosted_moccasins_and_runes", "boosted by moccasins and runes"); + } + if (equipped_feet == EQUIP_MOCCASINS) { + return i18n_lookup_key_with_fallback("system.character.speed.boosted_moccasins", "boosted by moccasins"); + } + if (rune_bonus > 0) { + return i18n_lookup_key_with_fallback("system.character.speed.boosted_runes", "boosted by runes"); + } + return i18n_lookup_key_with_fallback("system.character.speed.normal", "normal"); } void cleanup_equipment_after_inventory_change() { diff --git a/src/inventory_menus.nvgt b/src/inventory_menus.nvgt index 2422a4f..c5ee871 100644 --- a/src/inventory_menus.nvgt +++ b/src/inventory_menus.nvgt @@ -37,7 +37,7 @@ void check_inventory_keys(int x) { run_inventory_root_menu(); } else { if (in_base && world_storages.length() == 0) { - speak_with_history("No storage built.", true); + speak_with_history(tr("system.storage.no_storage_built"), true); } run_inventory_menu(false); } diff --git a/src/item_registry.nvgt b/src/item_registry.nvgt index 830db8e..33b43d7 100644 --- a/src/item_registry.nvgt +++ b/src/item_registry.nvgt @@ -281,20 +281,20 @@ int get_smoked_fish_yield(int weight) { // Item metadata lookups string get_item_label(int item_type) { if (item_type < 0 || item_type >= ITEM_COUNT) - return "items"; - return item_registry[item_type].name; + return i18n_lookup_key_with_fallback("system.item.label.items", "items"); + return i18n_translate_speech_message(item_registry[item_type].name); } string get_item_label_singular(int item_type) { if (item_type < 0 || item_type >= ITEM_COUNT) - return "item"; - return item_registry[item_type].singular; + return i18n_lookup_key_with_fallback("system.item.label.item", "item"); + return i18n_translate_speech_message(item_registry[item_type].singular); } string get_item_display_name(int item_type) { if (item_type < 0 || item_type >= ITEM_COUNT) - return "Unknown"; - return item_registry[item_type].display_name; + return i18n_lookup_key_with_fallback("system.item.label.unknown", "Unknown"); + return i18n_translate_speech_message(item_registry[item_type].display_name); } double get_item_favor_value(int item_type) { @@ -311,6 +311,28 @@ string get_item_label_for_count(int item_type, int count) { return get_item_label(item_type); } +string format_cant_carry_any_more_label(const string& in item_label) { + dictionary args; + args.set("item", item_label); + return trf("system.inventory.cant_carry_any_more_item", args); +} + +void speak_cant_carry_any_more_label(const string& in item_label) { + speak_with_history(format_cant_carry_any_more_label(item_label), true); +} + +void speak_cant_carry_any_more_item(int item_type) { + speak_cant_carry_any_more_label(get_item_label(item_type)); +} + +string get_need_quiver_for_arrows_message() { + return tr("system.inventory.need_quiver_for_arrows"); +} + +void speak_need_quiver_for_arrows() { + speak_with_history(get_need_quiver_for_arrows_message(), true); +} + // Encode runed item info into a negative item type // Format: -1000 - (equip_type * 10) - rune_type // This allows us to extract both equip_type and rune_type from a single int diff --git a/src/menus/action_menu.nvgt b/src/menus/action_menu.nvgt index c59224a..b62a934 100644 --- a/src/menus/action_menu.nvgt +++ b/src/menus/action_menu.nvgt @@ -260,6 +260,7 @@ void run_action_menu(int x) { options.insert_last("Check fishing pole"); action_types.insert_last(7); } + i18n_translate_string_array_in_place(options); string filter_text = ""; int[] filtered_indices; string[] filtered_options; diff --git a/src/menus/base_info.nvgt b/src/menus/base_info.nvgt index 9969c56..b214112 100644 --- a/src/menus/base_info.nvgt +++ b/src/menus/base_info.nvgt @@ -53,6 +53,7 @@ void run_base_info_menu() { } else { options.insert_last("Stable not built"); } + i18n_translate_string_array_in_place(options); string filter_text = ""; int[] filtered_indices; string[] filtered_options; diff --git a/src/menus/character_info.nvgt b/src/menus/character_info.nvgt index 305169a..a47243b 100644 --- a/src/menus/character_info.nvgt +++ b/src/menus/character_info.nvgt @@ -3,16 +3,18 @@ bool run_pet_abandon_confirm_menu() { if (!petActive) { - speak_with_history("No pet.", true); + speak_with_history(tr("system.character.pet.no_pet"), true); return false; } string[] options; - options.insert_last("No"); - options.insert_last("Yes"); + options.insert_last(tr("system.option.no")); + options.insert_last(tr("system.option.yes")); int selection = 0; - speak_with_history("Really abandon your pet? " + options[selection], true); + dictionary promptArgs; + promptArgs.set("option", options[selection]); + speak_with_history(trf("system.character.pet.abandon_confirm", promptArgs), true); while (true) { wait(5); @@ -20,7 +22,7 @@ bool run_pet_abandon_confirm_menu() { return false; } if (key_pressed(KEY_ESCAPE)) { - speak_with_history("Canceled.", true); + speak_with_history(tr("system.menu.canceled"), true); return false; } if (key_pressed(KEY_DOWN)) { @@ -42,7 +44,7 @@ bool run_pet_abandon_confirm_menu() { if (selection == 1) { return abandon_pet(); } - speak_with_history("Canceled.", true); + speak_with_history(tr("system.menu.canceled"), true); return false; } } @@ -55,63 +57,98 @@ void run_character_info_menu() { string[] missing_slots; if (equipped_head == EQUIP_HAT) - equipped_clothing.insert_last("Skin Hat"); + equipped_clothing.insert_last(get_base_equipment_name(EQUIP_HAT)); else - missing_slots.insert_last("head"); + missing_slots.insert_last(tr("system.character.slot.head")); if (equipped_torso == EQUIP_TUNIC) - equipped_clothing.insert_last("Skin Tunic"); + equipped_clothing.insert_last(get_base_equipment_name(EQUIP_TUNIC)); else - missing_slots.insert_last("torso"); + missing_slots.insert_last(tr("system.character.slot.torso")); if (equipped_arms == EQUIP_POUCH) - equipped_clothing.insert_last("Skin Pouch"); + equipped_clothing.insert_last(get_base_equipment_name(EQUIP_POUCH)); else if (equipped_arms == EQUIP_BACKPACK) - equipped_clothing.insert_last("Backpack"); + equipped_clothing.insert_last(get_base_equipment_name(EQUIP_BACKPACK)); else - missing_slots.insert_last("arms"); + missing_slots.insert_last(tr("system.character.slot.arms")); if (equipped_hands == EQUIP_GLOVES) - equipped_clothing.insert_last("Skin Gloves"); + equipped_clothing.insert_last(get_base_equipment_name(EQUIP_GLOVES)); else - missing_slots.insert_last("hands"); + missing_slots.insert_last(tr("system.character.slot.hands")); if (equipped_legs == EQUIP_PANTS) - equipped_clothing.insert_last("Skin Pants"); + equipped_clothing.insert_last(get_base_equipment_name(EQUIP_PANTS)); else - missing_slots.insert_last("legs"); + missing_slots.insert_last(tr("system.character.slot.legs")); if (equipped_feet == EQUIP_MOCCASINS) - equipped_clothing.insert_last("Moccasins"); + equipped_clothing.insert_last(get_base_equipment_name(EQUIP_MOCCASINS)); else - missing_slots.insert_last("feet"); + missing_slots.insert_last(tr("system.character.slot.feet")); string[] options; int petOptionIndex = -1; if (player_name != "") { - string sex_label = (player_sex == SEX_FEMALE) ? "Female" : "Male"; - options.insert_last("Name " + player_name + ". Sex " + sex_label + "."); + string sex_label = (player_sex == SEX_FEMALE) ? tr("system.sex.female") : tr("system.sex.male"); + dictionary nameArgs; + nameArgs.set("name", player_name); + nameArgs.set("sex", sex_label); + options.insert_last(trf("system.character.info.name_sex", nameArgs)); } else { - options.insert_last("Name unknown."); + options.insert_last(tr("system.character.info.name_unknown")); } - options.insert_last("Health " + player_health + " of " + max_health + "."); - options.insert_last("Weapon " + get_equipped_weapon_name() + "."); + + dictionary healthArgs; + healthArgs.set("health", player_health); + healthArgs.set("max", max_health); + options.insert_last(trf("system.character.info.health", healthArgs)); + + dictionary weaponArgs; + weaponArgs.set("weapon", get_equipped_weapon_name()); + options.insert_last(trf("system.character.info.weapon", weaponArgs)); + if (equipped_clothing.length() > 0) { - options.insert_last("Clothing equipped: " + join_string_list(equipped_clothing) + "."); + dictionary clothingArgs; + clothingArgs.set("items", join_string_list(equipped_clothing)); + options.insert_last(trf("system.character.info.clothing_equipped", clothingArgs)); } else { - options.insert_last("No clothing equipped."); + options.insert_last(tr("system.character.info.no_clothing")); } + if (missing_slots.length() > 0) { - options.insert_last("Missing " + join_string_list(missing_slots) + "."); + dictionary missingArgs; + missingArgs.set("slots", join_string_list(missing_slots)); + options.insert_last(trf("system.character.info.missing", missingArgs)); } - options.insert_last("Favor " + format_favor(favor) + "."); - options.insert_last("Speed " + get_speed_status() + "."); + + dictionary favorArgs; + favorArgs.set("favor", format_favor(favor)); + options.insert_last(trf("system.character.info.favor", favorArgs)); + + dictionary speedArgs; + speedArgs.set("status", get_speed_status()); + options.insert_last(trf("system.character.info.speed", speedArgs)); + if (petActive) { - string petInfo = "Pet " + get_pet_display_name() + ". " + petHealth + " health of " + PET_HEALTH_MAX + "."; - petInfo += " Loyalty " + petLoyalty + " of " + PET_LOYALTY_MAX + "."; + dictionary petArgs; + petArgs.set("pet", get_pet_display_name()); + petArgs.set("health", petHealth); + petArgs.set("max_health", PET_HEALTH_MAX); + petArgs.set("loyalty", petLoyalty); + petArgs.set("max_loyalty", PET_LOYALTY_MAX); + string petInfo = trf("system.character.info.pet", petArgs); + if (petKnockoutHoursRemaining > 0) { - string hourLabel = (petKnockoutHoursRemaining == 1) ? "hour" : "hours"; - petInfo += " Knocked out. " + petKnockoutHoursRemaining + " " + hourLabel + " remaining."; + dictionary petKnockoutArgs; + petKnockoutArgs.set("hours", petKnockoutHoursRemaining); + if (petKnockoutHoursRemaining == 1) { + petKnockoutArgs.set("hour_label", tr("system.character.word.hour")); + } else { + petKnockoutArgs.set("hour_label", tr("system.character.word.hours")); + } + petInfo += " " + trf("system.character.info.pet_knocked_out", petKnockoutArgs); } options.insert_last(petInfo); petOptionIndex = options.length() - 1; } else { - options.insert_last("Pet none."); + options.insert_last(tr("system.character.info.pet_none")); } int selection = 0; @@ -120,9 +157,11 @@ void run_character_info_menu() { string[] filtered_options; apply_menu_filter(filter_text, options, filtered_indices, filtered_options); if (filtered_options.length() == 0) { - speak_with_history("Character info. No options.", true); + speak_with_history(tr("system.character.menu.no_options"), true); } else { - speak_with_history("Character info. " + filtered_options[selection], true); + dictionary promptArgs; + promptArgs.set("option", filtered_options[selection]); + speak_with_history(trf("system.character.menu.prompt", promptArgs), true); } while (true) { @@ -131,7 +170,7 @@ void run_character_info_menu() { return; } if (key_pressed(KEY_ESCAPE)) { - speak_with_history("Closed.", true); + speak_with_history(tr("system.menu.closed"), true); break; } @@ -140,9 +179,11 @@ void run_character_info_menu() { if (filter_changed) { if (filtered_options.length() == 0) { if (filter_text.length() > 0) { - speak_with_history("No matches for " + filter_text + ".", true); + dictionary filterArgs; + filterArgs.set("arg1", filter_text); + speak_with_history(trf("system.menu.no_matches", filterArgs), true); } else { - speak_with_history("No options.", true); + speak_with_history(tr("system.menu.no_options"), true); } } else { speak_with_history(filtered_options[selection], true); @@ -176,8 +217,9 @@ void run_character_info_menu() { int optionIndex = filtered_indices[selection]; if (optionIndex == petOptionIndex && petActive) { if (is_pet_knocked_out()) { - speak_with_history( - "Your " + get_pet_display_name() + " is unconscious. You can't abandon it right now.", true); + dictionary args; + args.set("pet", get_pet_display_name()); + speak_with_history(trf("system.character.pet.unconscious_cannot_abandon", args), true); continue; } diff --git a/src/menus/equipment_menu.nvgt b/src/menus/equipment_menu.nvgt index 5aff5b3..938e79d 100644 --- a/src/menus/equipment_menu.nvgt +++ b/src/menus/equipment_menu.nvgt @@ -54,6 +54,12 @@ bool is_runed_item_equipped(int equip_type, int rune_type) { return get_equipped_rune_for_slot(equip_type) == rune_type; } +string get_equipment_menu_equipped_suffix(bool equipped) { + if (!equipped) + return ""; + return i18n_lookup_key_with_fallback("system.equipment.menu.equipped_suffix", " (equipped)"); +} + void run_equipment_menu() { speak_with_history("Equipment menu.", true); @@ -64,74 +70,74 @@ void run_equipment_menu() { // Add unruned items if (get_personal_count(ITEM_SPEARS) > 0) { - string status = is_runed_item_equipped(EQUIP_SPEAR, RUNE_NONE) ? " (equipped)" : ""; - options.insert_last("Spear" + status); + string status = get_equipment_menu_equipped_suffix(is_runed_item_equipped(EQUIP_SPEAR, RUNE_NONE)); + options.insert_last(get_full_equipment_name(EQUIP_SPEAR, RUNE_NONE) + status); equipment_types.insert_last(EQUIP_SPEAR); rune_types.insert_last(RUNE_NONE); } if (get_personal_count(ITEM_SLINGS) > 0) { - string status = is_runed_item_equipped(EQUIP_SLING, RUNE_NONE) ? " (equipped)" : ""; - options.insert_last("Sling" + status); + string status = get_equipment_menu_equipped_suffix(is_runed_item_equipped(EQUIP_SLING, RUNE_NONE)); + options.insert_last(get_full_equipment_name(EQUIP_SLING, RUNE_NONE) + status); equipment_types.insert_last(EQUIP_SLING); rune_types.insert_last(RUNE_NONE); } if (get_personal_count(ITEM_AXES) > 0) { - string status = is_runed_item_equipped(EQUIP_AXE, RUNE_NONE) ? " (equipped)" : ""; - options.insert_last("Stone Axe" + status); + string status = get_equipment_menu_equipped_suffix(is_runed_item_equipped(EQUIP_AXE, RUNE_NONE)); + options.insert_last(get_full_equipment_name(EQUIP_AXE, RUNE_NONE) + status); equipment_types.insert_last(EQUIP_AXE); rune_types.insert_last(RUNE_NONE); } if (get_personal_count(ITEM_BOWS) > 0) { - string status = is_runed_item_equipped(EQUIP_BOW, RUNE_NONE) ? " (equipped)" : ""; - options.insert_last("Bow" + status); + string status = get_equipment_menu_equipped_suffix(is_runed_item_equipped(EQUIP_BOW, RUNE_NONE)); + options.insert_last(get_full_equipment_name(EQUIP_BOW, RUNE_NONE) + status); equipment_types.insert_last(EQUIP_BOW); rune_types.insert_last(RUNE_NONE); } if (get_personal_count(ITEM_FISHING_POLES) > 0) { - string status = is_runed_item_equipped(EQUIP_FISHING_POLE, RUNE_NONE) ? " (equipped)" : ""; - options.insert_last("Fishing Pole" + status); + string status = get_equipment_menu_equipped_suffix(is_runed_item_equipped(EQUIP_FISHING_POLE, RUNE_NONE)); + options.insert_last(get_full_equipment_name(EQUIP_FISHING_POLE, RUNE_NONE) + status); equipment_types.insert_last(EQUIP_FISHING_POLE); rune_types.insert_last(RUNE_NONE); } if (get_personal_count(ITEM_SKIN_HATS) > 0) { - string status = is_runed_item_equipped(EQUIP_HAT, RUNE_NONE) ? " (equipped)" : ""; - options.insert_last("Skin Hat" + status); + string status = get_equipment_menu_equipped_suffix(is_runed_item_equipped(EQUIP_HAT, RUNE_NONE)); + options.insert_last(get_full_equipment_name(EQUIP_HAT, RUNE_NONE) + status); equipment_types.insert_last(EQUIP_HAT); rune_types.insert_last(RUNE_NONE); } if (get_personal_count(ITEM_SKIN_GLOVES) > 0) { - string status = is_runed_item_equipped(EQUIP_GLOVES, RUNE_NONE) ? " (equipped)" : ""; - options.insert_last("Skin Gloves" + status); + string status = get_equipment_menu_equipped_suffix(is_runed_item_equipped(EQUIP_GLOVES, RUNE_NONE)); + options.insert_last(get_full_equipment_name(EQUIP_GLOVES, RUNE_NONE) + status); equipment_types.insert_last(EQUIP_GLOVES); rune_types.insert_last(RUNE_NONE); } if (get_personal_count(ITEM_SKIN_PANTS) > 0) { - string status = is_runed_item_equipped(EQUIP_PANTS, RUNE_NONE) ? " (equipped)" : ""; - options.insert_last("Skin Pants" + status); + string status = get_equipment_menu_equipped_suffix(is_runed_item_equipped(EQUIP_PANTS, RUNE_NONE)); + options.insert_last(get_full_equipment_name(EQUIP_PANTS, RUNE_NONE) + status); equipment_types.insert_last(EQUIP_PANTS); rune_types.insert_last(RUNE_NONE); } if (get_personal_count(ITEM_SKIN_TUNICS) > 0) { - string status = is_runed_item_equipped(EQUIP_TUNIC, RUNE_NONE) ? " (equipped)" : ""; - options.insert_last("Skin Tunic" + status); + string status = get_equipment_menu_equipped_suffix(is_runed_item_equipped(EQUIP_TUNIC, RUNE_NONE)); + options.insert_last(get_full_equipment_name(EQUIP_TUNIC, RUNE_NONE) + status); equipment_types.insert_last(EQUIP_TUNIC); rune_types.insert_last(RUNE_NONE); } if (get_personal_count(ITEM_MOCCASINS) > 0) { - string status = is_runed_item_equipped(EQUIP_MOCCASINS, RUNE_NONE) ? " (equipped)" : ""; - options.insert_last("Moccasins" + status); + string status = get_equipment_menu_equipped_suffix(is_runed_item_equipped(EQUIP_MOCCASINS, RUNE_NONE)); + options.insert_last(get_full_equipment_name(EQUIP_MOCCASINS, RUNE_NONE) + status); equipment_types.insert_last(EQUIP_MOCCASINS); rune_types.insert_last(RUNE_NONE); } if (get_personal_count(ITEM_SKIN_POUCHES) > 0) { - string status = is_runed_item_equipped(EQUIP_POUCH, RUNE_NONE) ? " (equipped)" : ""; - options.insert_last("Skin Pouch" + status); + string status = get_equipment_menu_equipped_suffix(is_runed_item_equipped(EQUIP_POUCH, RUNE_NONE)); + options.insert_last(get_full_equipment_name(EQUIP_POUCH, RUNE_NONE) + status); equipment_types.insert_last(EQUIP_POUCH); rune_types.insert_last(RUNE_NONE); } if (get_personal_count(ITEM_BACKPACKS) > 0) { - string status = is_runed_item_equipped(EQUIP_BACKPACK, RUNE_NONE) ? " (equipped)" : ""; - options.insert_last("Backpack" + status); + string status = get_equipment_menu_equipped_suffix(is_runed_item_equipped(EQUIP_BACKPACK, RUNE_NONE)); + options.insert_last(get_full_equipment_name(EQUIP_BACKPACK, RUNE_NONE) + status); equipment_types.insert_last(EQUIP_BACKPACK); rune_types.insert_last(RUNE_NONE); } @@ -148,7 +154,7 @@ void run_equipment_menu() { if (count <= 0) continue; string name = get_full_equipment_name(equip_type, rune_type); - string status = is_runed_item_equipped(equip_type, rune_type) ? " (equipped)" : ""; + string status = get_equipment_menu_equipped_suffix(is_runed_item_equipped(equip_type, rune_type)); options.insert_last(name + status); equipment_types.insert_last(equip_type); rune_types.insert_last(rune_type); diff --git a/src/menus/inventory_core.nvgt b/src/menus/inventory_core.nvgt index ef3bff4..48653c1 100644 --- a/src/menus/inventory_core.nvgt +++ b/src/menus/inventory_core.nvgt @@ -1,6 +1,18 @@ // Personal inventory menu system // Functions for displaying and managing personal inventory +void speak_inventory_menu_prompt(const string& in option_label) { + dictionary args; + args.set("option", option_label); + speak_with_history(trf("system.inventory.menu.prompt", args), true); +} + +void speak_menu_no_matches(const string& in filter_text) { + dictionary args; + args.set("arg1", filter_text); + speak_with_history(trf("system.menu.no_matches", args), true); +} + void build_personal_inventory_options(string[] @options, int[] @item_types) { options.resize(0); item_types.resize(0); @@ -78,10 +90,9 @@ void show_inventory() { } void run_inventory_root_menu() { - speak_with_history("Inventory menu.", true); - int selection = 0; - string[] options = {"Personal inventory", "Base storage"}; + string[] options = {tr("system.inventory.option.personal"), tr("system.inventory.option.base_storage")}; + speak_inventory_menu_prompt(options[selection]); while (true) { wait(5); @@ -89,7 +100,7 @@ void run_inventory_root_menu() { return; } if (key_pressed(KEY_ESCAPE)) { - speak_with_history("Closed.", true); + speak_with_history(tr("system.menu.closed"), true); break; } @@ -121,8 +132,6 @@ void run_inventory_root_menu() { } void run_inventory_menu(bool allow_deposit) { - speak_with_history("Inventory menu.", true); - int selection = 0; string[] options; int[] item_types; @@ -131,6 +140,11 @@ void run_inventory_menu(bool allow_deposit) { int[] filtered_indices; string[] filtered_options; apply_menu_filter(filter_text, options, filtered_indices, filtered_options); + if (filtered_options.length() == 0) { + speak_with_history(tr("system.inventory.menu.no_options"), true); + } else { + speak_inventory_menu_prompt(filtered_options[selection]); + } while (true) { wait(5); @@ -138,7 +152,7 @@ void run_inventory_menu(bool allow_deposit) { return; } if (key_pressed(KEY_ESCAPE)) { - speak_with_history("Closed.", true); + speak_with_history(tr("system.menu.closed"), true); break; } @@ -147,9 +161,9 @@ void run_inventory_menu(bool allow_deposit) { if (filter_changed) { if (filtered_options.length() == 0) { if (filter_text.length() > 0) { - speak_with_history("No matches for " + filter_text + ".", true); + speak_menu_no_matches(filter_text); } else { - speak_with_history("No options.", true); + speak_with_history(tr("system.menu.no_options"), true); } } else { speak_with_history(filtered_options[selection], true); @@ -182,7 +196,10 @@ void run_inventory_menu(bool allow_deposit) { if (slot_index >= 0 && slot_index < int(item_count_slots.length())) { item_count_slots[slot_index] = item_type; string name = get_item_count_binding_name(item_type); - speak_with_history(name + " count set to slot " + slot_index + ".", true); + dictionary args; + args.set("name", name); + args.set("slot", slot_index); + speak_with_history(trf("system.inventory.count_set_to_slot", args), true); } } @@ -197,7 +214,7 @@ void run_inventory_menu(bool allow_deposit) { if (filtered_options.length() > 0) { speak_with_history(filtered_options[selection], true); } else if (filter_text.length() > 0) { - speak_with_history("No matches for " + filter_text + ".", true); + speak_menu_no_matches(filter_text); } } } @@ -213,7 +230,7 @@ void run_inventory_menu(bool allow_deposit) { if (filtered_options.length() > 0) { speak_with_history(filtered_options[selection], true); } else if (filter_text.length() > 0) { - speak_with_history("No matches for " + filter_text + ".", true); + speak_menu_no_matches(filter_text); } } } diff --git a/src/menus/storage_menu.nvgt b/src/menus/storage_menu.nvgt index cfbcd5a..ad62fa8 100644 --- a/src/menus/storage_menu.nvgt +++ b/src/menus/storage_menu.nvgt @@ -1,6 +1,18 @@ // Base storage menu system // Functions for interacting with base storage (deposit/withdraw items) +void speak_storage_menu_prompt(const string& in option_label) { + dictionary args; + args.set("option", option_label); + speak_with_history(trf("system.storage.menu.prompt", args), true); +} + +void speak_storage_menu_no_matches(const string& in filter_text) { + dictionary args; + args.set("arg1", filter_text); + speak_with_history(trf("system.menu.no_matches", args), true); +} + void move_small_game_to_storage(int amount) { for (int i = 0; i < amount; i++) { string game_type = "small game"; @@ -31,8 +43,11 @@ void move_fish_to_personal(int amount) { move_fish_weights_to_personal(amount); } -int prompt_transfer_amount(const string prompt, int max_amount) { - string input = ui_input_box("Inventory", prompt + " (max " + max_amount + ")", ""); +int prompt_transfer_amount(const string prompt_label, int max_amount) { + dictionary args; + args.set("prompt", prompt_label); + args.set("max", max_amount); + string input = ui_input_box(tr("system.storage.window_title"), trf("system.storage.transfer_prompt", args), ""); int amount = parse_int(input); if (amount <= 0) return 0; @@ -51,27 +66,27 @@ void deposit_item(int item_type) { cleanup_equipment_after_inventory_change(); speak_with_history("Deposited " + name + ".", true); } else { - speak_with_history("Nothing to deposit.", true); + speak_with_history(tr("system.storage.nothing_to_deposit"), true); } return; } // Handle legacy -2 marker (shouldn't happen anymore but keep for safety) if (item_type == -2) { - speak_with_history("Runed items cannot be deposited into storage.", true); + speak_with_history(tr("system.storage.runed_cannot_deposit"), true); return; } int available = get_personal_count(item_type); if (available <= 0) { - speak_with_history("Nothing to deposit.", true); + speak_with_history(tr("system.storage.nothing_to_deposit"), true); return; } int capacity = get_storage_stack_limit() - get_storage_count(item_type); if (capacity <= 0) { - speak_with_history("Storage for that item is full.", true); + speak_with_history(tr("system.storage.item_full"), true); return; } int max_transfer = (available < capacity) ? available : capacity; - int amount = prompt_transfer_amount("Deposit how many?", max_transfer); + int amount = prompt_transfer_amount(tr("system.storage.deposit_how_many"), max_transfer); if (amount <= 0) return; @@ -98,7 +113,7 @@ void deposit_item_max(int item_type) { decode_runed_item_type(item_type, equip_type, rune_type); int personal = get_runed_item_count(equip_type, rune_type); if (personal <= 0) { - speak_with_history("Nothing to deposit.", true); + speak_with_history(tr("system.storage.nothing_to_deposit"), true); return; } int count = 0; @@ -113,18 +128,18 @@ void deposit_item_max(int item_type) { } // Handle legacy -2 marker if (item_type == -2) { - speak_with_history("Runed items cannot be deposited into storage.", true); + speak_with_history(tr("system.storage.runed_cannot_deposit"), true); return; } int available = get_personal_count(item_type); if (available <= 0) { - speak_with_history("Nothing to deposit.", true); + speak_with_history(tr("system.storage.nothing_to_deposit"), true); return; } int capacity = get_storage_stack_limit() - get_storage_count(item_type); if (capacity <= 0) { - speak_with_history("Storage for that item is full.", true); + speak_with_history(tr("system.storage.item_full"), true); return; } @@ -155,31 +170,31 @@ void withdraw_item(int item_type) { string name = "Runed " + get_base_equipment_name(equip_type) + " of " + get_rune_effect_name(rune_type); speak_with_history("Withdrew " + name + ".", true); } else { - speak_with_history("Nothing to withdraw.", true); + speak_with_history(tr("system.storage.nothing_to_withdraw"), true); } return; } int available = get_storage_count(item_type); if (available <= 0) { - speak_with_history("Nothing to withdraw.", true); + speak_with_history(tr("system.storage.nothing_to_withdraw"), true); return; } int capacity = 0; if (item_type == ITEM_ARROWS) { capacity = get_arrow_limit() - get_personal_count(ITEM_ARROWS); if (capacity <= 0) { - speak_with_history("You can't carry any more arrows.", true); + speak_cant_carry_any_more_item(ITEM_ARROWS); return; } } else { capacity = get_personal_stack_limit() - get_personal_count(item_type); } if (capacity <= 0) { - speak_with_history("You can't carry any more " + get_item_label(item_type) + ".", true); + speak_cant_carry_any_more_item(item_type); return; } int max_transfer = (available < capacity) ? available : capacity; - int amount = prompt_transfer_amount("Withdraw how many?", max_transfer); + int amount = prompt_transfer_amount(tr("system.storage.withdraw_how_many"), max_transfer); if (amount <= 0) return; @@ -205,7 +220,7 @@ void withdraw_item_max(int item_type) { decode_runed_item_type(item_type, equip_type, rune_type); int stored = get_stored_runed_item_count(equip_type, rune_type); if (stored <= 0) { - speak_with_history("Nothing to withdraw.", true); + speak_with_history(tr("system.storage.nothing_to_withdraw"), true); return; } int count = 0; @@ -219,7 +234,7 @@ void withdraw_item_max(int item_type) { } int available = get_storage_count(item_type); if (available <= 0) { - speak_with_history("Nothing to withdraw.", true); + speak_with_history(tr("system.storage.nothing_to_withdraw"), true); return; } @@ -229,7 +244,7 @@ void withdraw_item_max(int item_type) { personalLimit = get_arrow_limit(); currentPersonal = get_personal_count(ITEM_ARROWS); if (personalLimit <= 0) { - speak_with_history("You need a quiver to carry arrows.", true); + speak_need_quiver_for_arrows(); return; } } else { @@ -239,7 +254,7 @@ void withdraw_item_max(int item_type) { int space = personalLimit - currentPersonal; if (space <= 0) { - speak_with_history("Can't carry any more.", true); + speak_cant_carry_any_more_item(item_type); return; } @@ -293,12 +308,10 @@ void build_storage_inventory_options(string[] @options, int[] @item_types) { void run_storage_menu() { if (world_storages.length() == 0) { - speak_with_history("No storage built.", true); + speak_with_history(tr("system.storage.no_storage_built"), true); return; } - speak_with_history("Base storage.", true); - int selection = 0; string[] options; int[] item_types; @@ -307,6 +320,11 @@ void run_storage_menu() { int[] filtered_indices; string[] filtered_options; apply_menu_filter(filter_text, options, filtered_indices, filtered_options); + if (filtered_options.length() == 0) { + speak_with_history(tr("system.storage.menu.no_options"), true); + } else { + speak_storage_menu_prompt(filtered_options[selection]); + } while (true) { wait(5); @@ -314,7 +332,7 @@ void run_storage_menu() { return; } if (key_pressed(KEY_ESCAPE)) { - speak_with_history("Closed.", true); + speak_with_history(tr("system.menu.closed"), true); break; } @@ -323,9 +341,9 @@ void run_storage_menu() { if (filter_changed) { if (filtered_options.length() == 0) { if (filter_text.length() > 0) { - speak_with_history("No matches for " + filter_text + ".", true); + speak_storage_menu_no_matches(filter_text); } else { - speak_with_history("No options.", true); + speak_with_history(tr("system.menu.no_options"), true); } } else { speak_with_history(filtered_options[selection], true); @@ -363,7 +381,7 @@ void run_storage_menu() { if (filtered_options.length() > 0) { speak_with_history(filtered_options[selection], true); } else if (filter_text.length() > 0) { - speak_with_history("No matches for " + filter_text + ".", true); + speak_storage_menu_no_matches(filter_text); } } } @@ -379,7 +397,7 @@ void run_storage_menu() { if (filtered_options.length() > 0) { speak_with_history(filtered_options[selection], true); } else if (filter_text.length() > 0) { - speak_with_history("No matches for " + filter_text + ".", true); + speak_storage_menu_no_matches(filter_text); } } } diff --git a/src/pet_system.nvgt b/src/pet_system.nvgt index 3303fc5..bd872e9 100644 --- a/src/pet_system.nvgt +++ b/src/pet_system.nvgt @@ -228,10 +228,19 @@ int get_pet_listener_pos() { } string get_health_report() { - string report = player_health + " health of " + max_health; + dictionary playerArgs; + playerArgs.set("health", player_health); + playerArgs.set("max", max_health); + string report = trf("system.character.report.player", playerArgs); + if (petActive) { - report += ", " + get_pet_display_name() + ", " + petHealth + " health of " + PET_HEALTH_MAX; - report += ", loyalty " + petLoyalty + " of " + PET_LOYALTY_MAX; + dictionary petArgs; + petArgs.set("pet", get_pet_display_name()); + petArgs.set("health", petHealth); + petArgs.set("max", PET_HEALTH_MAX); + petArgs.set("loyalty", petLoyalty); + petArgs.set("max_loyalty", PET_LOYALTY_MAX); + report += ", " + trf("system.character.report.pet", petArgs); } return report; } @@ -891,6 +900,7 @@ bool run_pet_offer_menu(const string& in soundPath, const string& in reasonText) string[] options; options.insert_last("Yes"); options.insert_last("No"); + i18n_translate_string_array_in_place(options); int selection = 0; speak_with_history(prompt + " " + options[selection], true); diff --git a/src/quest_system.nvgt b/src/quest_system.nvgt index b6da03a..24ea059 100644 --- a/src/quest_system.nvgt +++ b/src/quest_system.nvgt @@ -175,7 +175,7 @@ void apply_quest_reward(int score) { } message += "\nScore: " + score; - text_reader(message, "Quest Rewards", true); + text_reader(message, i18n_text("Quest Rewards"), true); attempt_pet_offer_from_quest(score); } diff --git a/src/quests/bat_invasion_game.nvgt b/src/quests/bat_invasion_game.nvgt index 710d744..9d0812d 100644 --- a/src/quests/bat_invasion_game.nvgt +++ b/src/quests/bat_invasion_game.nvgt @@ -9,13 +9,14 @@ int run_bat_invasion() { instructions.insert_last("Bats are invading! Throw your spear to defend."); instructions.insert_last(""); instructions.insert_last("How to play:"); - instructions.insert_last(" - Listen for bats flying past from left or right"); - instructions.insert_last(" - Press SPACE to throw your spear when the bat sounds centered"); - instructions.insert_last(" - Accurate throws earn 2 points each"); - instructions.insert_last(" - You have 10 attempts"); + instructions.insert_last("- Listen for bats flying past from left or right"); + instructions.insert_last("- Press SPACE to throw your spear when the bat sounds centered"); + instructions.insert_last("- Accurate throws earn 2 points each"); + instructions.insert_last("- You have 10 attempts"); instructions.insert_last(""); instructions.insert_last("Close this screen to begin."); - text_reader_lines(instructions, "Quest Instructions", true); + i18n_translate_string_array_in_place(instructions); + text_reader_lines(instructions, i18n_text("Quest Instructions"), true); speak_with_history("Starting.", true); wait(500); diff --git a/src/quests/catch_the_boomerang_game.nvgt b/src/quests/catch_the_boomerang_game.nvgt index d23a2d2..3964535 100644 --- a/src/quests/catch_the_boomerang_game.nvgt +++ b/src/quests/catch_the_boomerang_game.nvgt @@ -6,13 +6,14 @@ int run_catch_the_boomerang() { instructions.insert_last("Throw a boomerang and catch it on the return."); instructions.insert_last(""); instructions.insert_last("How to play:"); - instructions.insert_last(" - Press Space to throw"); - instructions.insert_last(" - Press Space again when it sounds close"); - instructions.insert_last(" - There are 5 turns"); - instructions.insert_last(" - Better timing earns more points (up to 4)"); + instructions.insert_last("- Press Space to throw"); + instructions.insert_last("- Press Space again when it sounds close"); + instructions.insert_last("- There are 5 turns"); + instructions.insert_last("- Better timing earns more points (up to 4)"); instructions.insert_last(""); instructions.insert_last("Close this screen to begin."); - text_reader_lines(instructions, "Quest Instructions", true); + i18n_translate_string_array_in_place(instructions); + text_reader_lines(instructions, i18n_text("Quest Instructions"), true); wait(500); diff --git a/src/quests/enchanted_melody_game.nvgt b/src/quests/enchanted_melody_game.nvgt index a8e19f0..513999d 100644 --- a/src/quests/enchanted_melody_game.nvgt +++ b/src/quests/enchanted_melody_game.nvgt @@ -28,14 +28,15 @@ void run_practice_mode() { instructions.insert_last("Repeat the magical pattern to earn favor from the gods."); instructions.insert_last(""); instructions.insert_last("Controls:"); - instructions.insert_last(" F or K - First note (lowest pitch)"); - instructions.insert_last(" D or J - Second note"); - instructions.insert_last(" R or I - Third note"); - instructions.insert_last(" E or U - Fourth note (highest pitch)"); + instructions.insert_last("F or K - First note (lowest pitch)"); + instructions.insert_last("D or J - Second note"); + instructions.insert_last("R or I - Third note"); + instructions.insert_last("E or U - Fourth note (highest pitch)"); instructions.insert_last(""); instructions.insert_last("After closing this screen, you can practice the notes."); instructions.insert_last("Press Enter when ready to begin, or Escape to cancel."); - text_reader_lines(instructions, "Quest Instructions", true); + i18n_translate_string_array_in_place(instructions); + text_reader_lines(instructions, i18n_text("Quest Instructions"), true); // Practice mode announcement speak_with_history("Practice mode. Press Enter to begin, Escape to cancel.", true); diff --git a/src/quests/escape_from_hel_game.nvgt b/src/quests/escape_from_hel_game.nvgt index 35387af..7fff9c2 100644 --- a/src/quests/escape_from_hel_game.nvgt +++ b/src/quests/escape_from_hel_game.nvgt @@ -8,14 +8,15 @@ int run_escape_from_hel() { instructions.insert_last("You have plundered the treasure, and the Draugr are displeased.!"); instructions.insert_last(""); instructions.insert_last("How to play:"); - instructions.insert_last(" - You run automatically, speed increases over time"); - instructions.insert_last(" - Listen for open graves approaching (growing louder)"); - instructions.insert_last(" - Press SPACE to jump over graves"); - instructions.insert_last(" - Each successful jump earns 2 points"); - instructions.insert_last(" - The run ends when you fall into a grave"); + instructions.insert_last("- You run automatically, speed increases over time"); + instructions.insert_last("- Listen for open graves approaching (growing louder)"); + instructions.insert_last("- Press SPACE to jump over graves"); + instructions.insert_last("- Each successful jump earns 2 points"); + instructions.insert_last("- The run ends when you fall into a grave"); instructions.insert_last(""); instructions.insert_last("Close this screen to begin."); - text_reader_lines(instructions, "Quest Instructions", true); + i18n_translate_string_array_in_place(instructions); + text_reader_lines(instructions, i18n_text("Quest Instructions"), true); wait(500); diff --git a/src/quests/skeletal_bard_game.nvgt b/src/quests/skeletal_bard_game.nvgt index 3edf7bd..513de0a 100644 --- a/src/quests/skeletal_bard_game.nvgt +++ b/src/quests/skeletal_bard_game.nvgt @@ -45,13 +45,14 @@ int run_skeletal_bard() { instructions.insert_last("Listen to the melody and count the notes."); instructions.insert_last(""); instructions.insert_last("How to play:"); - instructions.insert_last(" - After the tune, choose the number of notes"); - instructions.insert_last(" - Use Up/Down to change the number"); - instructions.insert_last(" - Press Enter to confirm"); - instructions.insert_last(" - There are 5 rounds, up to 4 points each"); + instructions.insert_last("- After the tune, choose the number of notes"); + instructions.insert_last("- Use Up/Down to change the number"); + instructions.insert_last("- Press Enter to confirm"); + instructions.insert_last("- There are 5 rounds, up to 4 points each"); instructions.insert_last(""); instructions.insert_last("Close this screen to begin."); - text_reader_lines(instructions, "Quest Instructions", true); + i18n_translate_string_array_in_place(instructions); + text_reader_lines(instructions, i18n_text("Quest Instructions"), true); wait(500); diff --git a/src/save_system.nvgt b/src/save_system.nvgt index c699261..f45b68b 100644 --- a/src/save_system.nvgt +++ b/src/save_system.nvgt @@ -1,4 +1,8 @@ // Save system +#include "libstorm-nvgt/crash_logger.nvgt" +#include "libstorm-nvgt/dict_utils.nvgt" +#include "libstorm-nvgt/name_sanitize.nvgt" +#include "libstorm-nvgt/save_utils.nvgt" const string SAVE_EXTENSION = ".dat"; const string SAVE_ENCRYPTION_KEY = "draugnorak_save_v1"; @@ -145,7 +149,7 @@ string[] get_save_files() { } bool sort_string_case_insensitive(const string& in a, const string& in b) { - return a.lower() < b.lower(); + return save_utils_sort_string_case_insensitive(a, b); } bool has_save_game() { @@ -153,244 +157,67 @@ bool has_save_game() { } string encrypt_save_data(const string& in rawData) { - return string_aes_encrypt(rawData, SAVE_ENCRYPTION_KEY); + return encrypt_string_aes(rawData, SAVE_ENCRYPTION_KEY); } string decrypt_save_data(const string& in encryptedData) { - return string_aes_decrypt(encryptedData, SAVE_ENCRYPTION_KEY); + return decrypt_string_aes(encryptedData, SAVE_ENCRYPTION_KEY); } bool save_data(const string& in filename, const string& in data) { - if (data.length() == 0) { - return false; - } - string resolvedPath = resolve_save_path(filename); - file tmp; - if (!tmp.open(resolvedPath, "wb")) { - return false; - } - - if (tmp.write(data) < data.length()) { - tmp.close(); - return false; - } - - tmp.close(); - return true; + return save_string_file(resolve_save_path(filename), data); } bool read_file_string(const string& in filename, string& out data) { - string resolvedPath = resolve_save_path(filename); - file tmp; - if (!tmp.open(resolvedPath, "rb")) { - return false; - } - data = tmp.read(); - tmp.close(); - return data.length() > 0; + return read_string_file(resolve_save_path(filename), data, false); } double get_number(dictionary @data, const string& in key, double defaultValue) { - double value; - if (@data == null) - return defaultValue; - if (data.get(key, value)) - return value; - int value_int; - if (data.get(key, value_int)) - return value_int; - string value_str; - if (data.get(key, value_str)) { - return parse_int(value_str); - } - return defaultValue; + return dict_get_number(data, key, defaultValue); } bool get_bool(dictionary @data, const string& in key, bool defaultValue) { - bool value; - if (@data == null) - return defaultValue; - if (data.get(key, value)) - return value; - int value_int; - if (data.get(key, value_int)) - return value_int != 0; - string value_str; - if (data.get(key, value_str)) - return value_str == "1" || value_str == "true"; - return defaultValue; + return dict_get_bool(data, key, defaultValue); } bool dictionary_has_keys(dictionary @data) { - if (@data == null) - return false; - string[] @keys = data.get_keys(); - return keys.length() > 0; + return dict_has_keys(data); } bool has_number_key(dictionary @data, const string& in key) { - double value; - if (@data == null) - return false; - if (data.get(key, value)) - return true; - int value_int; - if (data.get(key, value_int)) - return true; - string value_str; - if (data.get(key, value_str)) - return value_str.length() > 0; - return false; + return dict_has_number_key(data, key); } string[] get_string_list(dictionary @data, const string& in key) { - string[] result; - if (@data == null) - return result; - if (!data.get(key, result)) - return result; - return result; + return dict_get_string_list(data, key); } string flatten_exception_text(const string& in text) { - string result = ""; - bool lastWasSpace = false; - for (uint i = 0; i < text.length(); i++) { - string ch = text.substr(i, 1); - if (ch == "\r" || ch == "\n") { - if (!lastWasSpace) { - result += " | "; - lastWasSpace = true; - } - continue; - } - result += ch; - lastWasSpace = false; - } - return result; + return log_flatten_multiline(text); } string format_log_timestamp() { - datetime dt; - string stamp = dt.format(DATE_TIME_FORMAT_RFC1123); - return "[" + stamp + "]"; + return log_format_timestamp(); } void log_unhandled_exception(const string& in context) { - string info = get_exception_info(); - string filePath = get_exception_file(); - int line = get_exception_line(); - string func = get_exception_function(); - string stack = flatten_exception_text(last_exception_call_stack); - - string message = "Unhandled exception"; - if (context != "") - message += " (" + context + ")"; - if (info != "") - message += ": " + info; - if (filePath != "") - message += " at " + filePath; - if (line > 0) - message += ":" + line; - if (func != "") - message += " in " + func; - if (stack != "") - message += " | stack: " + stack; - message += " " + format_log_timestamp(); - - file logFile; - if (logFile.open(get_crash_log_path(), "ab")) { - logFile.write(message + "\r\n"); - logFile.close(); - } + log_unhandled_exception_to_file(get_crash_log_path(), context); } string normalize_player_name(string name) { - string result = ""; - bool lastWasSpace = true; - for (uint i = 0; i < name.length(); i++) { - string ch = name.substr(i, 1); - bool isSpace = (ch == " " || ch == "\t" || ch == "\r" || ch == "\n"); - if (isSpace) { - if (!lastWasSpace) { - result += " "; - lastWasSpace = true; - } - continue; - } - result += ch; - lastWasSpace = false; - } - if (result.length() > 0 && result.substr(result.length() - 1) == " ") { - result = result.substr(0, result.length() - 1); - } - return result; + return normalize_name_whitespace(name); } bool is_windows_reserved_name(const string& in upperName) { - if (upperName == "CON" || upperName == "PRN" || upperName == "AUX" || upperName == "NUL") - return true; - if (upperName.length() == 4 && upperName.substr(0, 3) == "COM") { - int num = parse_int(upperName.substr(3)); - if (num >= 1 && num <= 9) - return true; - } - if (upperName.length() == 4 && upperName.substr(0, 3) == "LPT") { - int num = parse_int(upperName.substr(3)); - if (num >= 1 && num <= 9) - return true; - } - return false; + return is_windows_reserved_filename(upperName); } string sanitize_save_filename_base(string name) { - string normalized = normalize_player_name(name); - string result = ""; - bool lastSeparator = false; - - for (uint i = 0; i < normalized.length(); i++) { - string ch = normalized.substr(i, 1); - bool isUpper = (ch >= "A" && ch <= "Z"); - bool isLower = (ch >= "a" && ch <= "z"); - bool isDigit = (ch >= "0" && ch <= "9"); - if (isUpper || isLower || isDigit) { - result += ch; - lastSeparator = false; - continue; - } - if (ch == " " || ch == "_" || ch == "-") { - if (!lastSeparator && result.length() > 0) { - result += "_"; - lastSeparator = true; - } - } - } - - while (result.length() > 0 && result.substr(result.length() - 1) == "_") { - result = result.substr(0, result.length() - 1); - } - if (result.length() == 0) { - result = "character"; - } - if (result.length() > 40) { - result = result.substr(0, 40); - } - while (result.length() > 0 && result.substr(result.length() - 1) == "_") { - result = result.substr(0, result.length() - 1); - } - string upperName = result.upper(); - if (upperName == "." || upperName == "..") { - result = "character"; - upperName = result.upper(); - } - if (is_windows_reserved_name(upperName)) { - result = "save_" + result; - } - return result; + return sanitize_filename_base_with_reserved_prefix(name, 40, "character", "save_"); } string get_save_filename_for_name(const string& in name) { - return sanitize_save_filename_base(name) + SAVE_EXTENSION; + return build_filename_from_name_ex(name, SAVE_EXTENSION, 40, "character", "save_"); } string strip_save_extension(const string& in filename) { @@ -504,10 +331,16 @@ string pick_random_name_for_sex(int sex, const string[] @usedNames) { return pick_random_name(male_name_pool, usedNames); } +string get_localized_sex_label(int sexValue) { + if (sexValue == SEX_FEMALE) + return tr("system.sex.female"); + return tr("system.sex.male"); +} + bool select_player_sex(int& out sex) { - string[] options = {"Male", "Female"}; + string[] options = {get_localized_sex_label(SEX_MALE), get_localized_sex_label(SEX_FEMALE)}; int selection = 0; - string prompt = "Choose your sex."; + string prompt = tr("system.new_character.choose_sex"); speak_with_history(prompt + " " + options[selection], true); while (true) { @@ -548,14 +381,16 @@ bool setup_new_character() { string[] existingNames = get_existing_character_names(); while (true) { - string entered = ui_input_box("Draugnorak", "Enter your name or press Enter for random.", ""); + string entered = ui_input_box(tr("system.ui.window_title"), tr("system.new_character.enter_name"), ""); string normalized = normalize_player_name(entered); if (normalized.length() == 0) { normalized = pick_random_name_for_sex(selectedSex, existingNames); } string saveFile = get_save_filename_for_name(normalized); if (file_exists(resolve_save_path(saveFile))) { - int confirm = ui_question("", "Save found for " + normalized + ". Overwrite?"); + dictionary overwriteArgs; + overwriteArgs.set("name", normalized); + int confirm = ui_question("", trf("system.new_character.save_exists_overwrite", overwriteArgs)); if (confirm != 1) { continue; } @@ -588,8 +423,11 @@ bool select_save_file(string& out filename) { displayName = strip_save_extension(files[i]); options.insert_last(displayName); } else { - string sex_label = (sex == SEX_FEMALE) ? "female" : "male"; - options.insert_last(displayName + ", " + sex_label + ", day " + day); + dictionary optionArgs; + optionArgs.set("name", displayName); + optionArgs.set("sex", get_localized_sex_label(sex)); + optionArgs.set("day", day); + options.insert_last(trf("system.load_game.option_with_metadata", optionArgs)); } displayNames.insert_last(displayName); sexValues.insert_last(sex); @@ -624,12 +462,13 @@ bool select_save_file(string& out filename) { return true; } if (key_pressed(KEY_DELETE)) { - string prompt = "Are you sure you want to delete the character " + displayNames[selection]; + dictionary deleteArgs; + deleteArgs.set("name", displayNames[selection]); + string prompt = trf("system.load_game.delete_confirm_base", deleteArgs); if (hasMetadata[selection]) { - string sex_label = (sexValues[selection] == SEX_FEMALE) ? "female" : "male"; - prompt += " gender " + sex_label + " days " + dayValues[selection] + "?"; - } else { - prompt += "?"; + deleteArgs.set("sex", get_localized_sex_label(sexValues[selection])); + deleteArgs.set("day", dayValues[selection]); + prompt = trf("system.load_game.delete_confirm_with_metadata", deleteArgs); } int confirm = ui_question("", prompt); if (confirm == 1) { @@ -638,7 +477,8 @@ bool select_save_file(string& out filename) { refreshList = true; break; } else { - ui_info_box("Draugnorak", "Delete Save", "Unable to delete save."); + ui_info_box(tr("system.ui.window_title"), tr("system.load_game.delete_save_heading"), + tr("system.load_game.delete_save_failed")); speak_with_history(options[selection], true); } } else { diff --git a/src/text_reader_aliases.nvgt b/src/text_reader_aliases.nvgt index 13340be..0133358 100644 --- a/src/text_reader_aliases.nvgt +++ b/src/text_reader_aliases.nvgt @@ -1,13 +1,13 @@ // Compatibility aliases that keep legacy text_reader* callsites unchanged. // file_viewer* is provided by libstorm-nvgt/docs_browser.nvgt. string text_reader(string content, string title = "Text Reader", bool readOnly = true) { - return file_viewer(content, title, readOnly); + return file_viewer(content, i18n_text(title), readOnly); } string text_reader_lines(string[] lines, string title = "Text Reader", bool readOnly = true) { - return file_viewer_lines(lines, title, readOnly); + return file_viewer_lines(lines, i18n_text(title), readOnly); } string text_reader_file(string filePath, string title = "", bool readOnly = true) { - return file_viewer_file(filePath, title, readOnly); + return file_viewer_file(filePath, i18n_text(title), readOnly); } diff --git a/src/world/world_drops.nvgt b/src/world/world_drops.nvgt index 3d3d78c..c6be598 100644 --- a/src/world/world_drops.nvgt +++ b/src/world/world_drops.nvgt @@ -91,12 +91,14 @@ void clear_world_drops() { bool try_pickup_small_game(string game_type) { if (get_personal_count(ITEM_SMALL_GAME) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more small game.", true); + speak_cant_carry_any_more_item(ITEM_SMALL_GAME); return false; } add_personal_count(ITEM_SMALL_GAME, 1); personal_small_game_types.insert_last(game_type); - speak_with_history("Picked up " + game_type + ".", true); + dictionary args; + args.set("item", i18n_translate_speech_message(game_type)); + speak_with_history(trf("system.pickup.item", args), true); return true; } @@ -107,26 +109,32 @@ bool try_pickup_world_drop(WorldDrop @drop) { if (drop.type == "arrow") { int max_arrows = get_arrow_limit(); if (max_arrows <= 0) { - speak_with_history("You need a quiver to carry arrows.", true); + speak_need_quiver_for_arrows(); return false; } if (get_personal_count(ITEM_ARROWS) >= max_arrows) { - speak_with_history("You can't carry any more arrows.", true); + speak_cant_carry_any_more_item(ITEM_ARROWS); return false; } add_personal_count(ITEM_ARROWS, 1); - speak_with_history("Picked up arrow.", true); + dictionary args; + args.set("item", get_item_label_singular(ITEM_ARROWS)); + speak_with_history(trf("system.pickup.item", args), true); return true; } if (drop.type == "boar carcass") { if (get_personal_count(ITEM_BOAR_CARCASSES) >= get_personal_stack_limit()) { - speak_with_history("You can't carry any more boar carcasses.", true); + speak_cant_carry_any_more_item(ITEM_BOAR_CARCASSES); return false; } add_personal_count(ITEM_BOAR_CARCASSES, 1); - speak_with_history("Picked up boar carcass.", true); + dictionary args; + args.set("item", get_item_label_singular(ITEM_BOAR_CARCASSES)); + speak_with_history(trf("system.pickup.item", args), true); return true; } - speak_with_history("Picked up " + drop.type + ".", true); + dictionary args; + args.set("item", i18n_translate_speech_message(drop.type)); + speak_with_history(trf("system.pickup.item", args), true); return true; } diff --git a/todo.txt b/todo.txt new file mode 100644 index 0000000..7799fd6 --- /dev/null +++ b/todo.txt @@ -0,0 +1,72 @@ +Late-Game Undead Concept (Design Only, No Code Yet) + +Name Lock +- Final name: Draugrhaugr. +- Meaning fit: undead + grave mound/barrow. + +Core Fantasy +- A single colossal undead mass made from many bodies in a grave-mound pile. +- Slow, relentless, and extremely dangerous if it reaches the player or barricade. +- Endgame pressure unit, not a common enemy. + +Spawn Rules +- Earliest appearance: Day 20. +- Night-only spawn. +- Max active at once: 1. +- Max spawns per night: 1. +- Start with low chance at day 20, then ramp slowly by day. +- Suggested chance curve (draft): + - Day 20: 4% per eligible spawn check Increases by 4% daily to 100% + +Encounter Notifications +- First warning (pre-spawn): "Something large is causing the ground to buckle and heave." +- Suggested warning sound: small avalanche/earth-shift style rumble. +- Spawn notify line: "A Draugrhaugr has been spotted." + +Stats (Draft Targets) +- Health: 125 baseline (allowed tuning range 100-150). +- Attack damage: 20-25 per hit. +- Attack range: 3 tiles. +- Move speed: slow (slower than normal zombies/bandits). +- Intended threat profile: low frequency, high punishment. + +Behavior (Draft) +- Prioritizes player when player is outside base and in range. +- Otherwise advances toward base/barricade. +- Can attack barricade from its attack range once in range. +- Does not use standard undead terrain footstep switching. +- Can consume nearby undead and absorb them into the mass: + - Valid consume targets: zombies, wights, vampyrs. + - Consumed target is removed. + - Draugrhaugr heals when consume succeeds. + +Audio Requirements +- Movement step sound: one giant stomp sound for all terrain. sounds/terrain/draugrhaugr_step.ogg +- No terrain-dependent step audio. + +Sound Asset Checklist (Gather Before Implementation) +- `sounds/enemies/draugrhaugr1.ogg` (required) +- `sounds/enemies/draugrhaugr_attack.ogg` (required) +- `sounds/enemies/draugrhaugr_hit.ogg` (required) +- `sounds/enemies/draugrhaugr_consume.ogg` (required) +- `sounds/enemies/draugrhaugr_dies.ogg` (required) +- `sounds/enemies/draugrhaugr_warning.ogg` (recommended pre-spawn avalanche/ground buckle cue) +- `sounds/enemies/draugrhaugr_spawn.ogg` (optional emergence sound) + +Balance/Encounter Goals +- Should feel like a "night boss-lite" event, not routine pressure. +- Player should have time to react due to slow speed. +- If ignored, it causes severe consequences quickly. + +Open Decisions Before Implementation +- Final HP target: 100, 125, or 150. +- Final attack damage model: fixed 20, fixed 25, or random 20-25. +- Exact spawn chance curve and whether chance is per night or per hourly check. +- Consume heal amount per target and whether it differs by target type. +- Consume range/cooldown. +- Whether kill reward should be special (favor, rare drop, both, or none). +- Whether it should have special resistances or just raw stats. +- How many numbered voice variants to support at launch (minimum 3 suggested). + +Implementation Note +- When implementation starts, treat this as a separate undead type with strict singleton/night spawn limits, dedicated stomp audio handling, and explicit consume-heal logic for undead targets. diff --git a/translate.md b/translate.md new file mode 100644 index 0000000..3dfed4f --- /dev/null +++ b/translate.md @@ -0,0 +1,206 @@ +# How To Translate Draugnorak (Beginner Guide) + +This guide is for first-time translators. +You do not need to know programming. + +## What You Are Editing + +Draugnorak stores text in translation files inside the `lang/` folder. + +Important files: +- `lang/en.template.ini` -> a clean template you copy from. +- `lang/en.ini` -> English source text used by the game as fallback. +- `lang/.ini` -> your translation file. + +Example language codes: +- `es` for Spanish +- `fr` for French +- `de` for German +- `pt-BR` for Brazilian Portuguese + +## Before You Start + +You need: +1. A plain text editor (VS Code, Notepad++, Kate, etc.). +2. This project folder on your machine. +3. Basic comfort editing text files. + +## Step 1: Make Your Translation File + +1. Open the `lang` folder. +2. Copy `en.template.ini`. +3. Rename the copy to your language code, for example: + - `es.ini` + +You now have a file like `lang/es.ini`. + +## Step 2: Fill In Language Metadata + +At the top of the file, find: + +```ini +[meta] +code=en +name=English +native_name=English +``` + +Change these values for your language. + +Example for Spanish: + +```ini +[meta] +code=es +name=Spanish +native_name=Espanol +``` + +Notes: +- `code` should match your filename (`es.ini` -> `code=es`). +- `name` can be in English. +- `native_name` should be in your own language (what speakers call it). + +## Step 3: Translate Only the Right Side + +Every line is `key=value`. + +Example: + +```ini +msg.528fb1c4e5fb=New Game +``` + +You only translate the value after `=`: + +```ini +msg.528fb1c4e5fb=Nuevo juego +``` + +Do NOT change: +- the key (`msg.528fb1c4e5fb`) +- section names like `[messages]` +- file structure + +## Step 4: Keep Placeholders Exactly As Written + +Some messages contain placeholders like: +- `{arg1}` +- `{arg2}` +- `{language}` +- `{count}` + +Example: + +```ini +system.language.selected=Language set to {language}. +``` + +Good translation: + +```ini +system.language.selected=Idioma configurado en {language}. +``` + +Rules: +- Keep placeholder names exactly the same. +- You can move them in the sentence. +- Do not remove them. +- Do not rename them. + +Bad examples: +- `... {lang}` (wrong name) +- missing placeholder entirely + +## Step 5: Keep Escapes Intact + +Some values may contain escaped characters: +- `\n` = new line +- `\t` = tab + +If you see these, keep them unless you know exactly why to change them. + +## Step 6: Save as UTF-8 + +Use UTF-8 encoding so accented and non-Latin characters are stored correctly. + +Most modern editors use UTF-8 by default. + +## Step 7: Validate Your Translation + +From the project root, run: + +```bash +python3 scripts/validate_i18n_catalog.py +``` + +What this checks: +- missing keys +- extra keys +- placeholder mismatches + +If it prints `OK`, your file structure is valid. +If it prints `FAIL`, read the listed errors and fix them. + +## Step 8: Test In-Game + +On first launch, the game asks for language selection if no saved language preference exists. +Choose your language and check menus/messages. + +If language was already saved before, you may need to remove or edit the saved preference file so first-run selection appears again. +(Preference is stored in `draugnorak.dat` under the appdata config directory.) + +## Common Mistakes + +1. Editing keys instead of values. +2. Deleting `{arg1}` placeholders. +3. Renaming section headers. +4. Saving in a non-UTF-8 encoding. +5. Forgetting to run validator script. + +## Translation Style Tips + +1. Prefer clear, simple text over literal word-for-word translation. +2. Keep gameplay warnings short and urgent. +3. Keep menu labels concise. +4. Keep terminology consistent (same item name everywhere). + +## Example Mini Translation + +Original: + +```ini +msg.528fb1c4e5fb=New Game +msg.802aa6576577=Load Game +msg.f83b6fe3aebf=Exit +``` + +Spanish example: + +```ini +msg.528fb1c4e5fb=Nuevo juego +msg.802aa6576577=Cargar partida +msg.f83b6fe3aebf=Salir +``` + +## If You Are Unsure About a Line + +If one sentence is unclear: +1. Leave it in English for now. +2. Add a comment above it using `;`. +3. Continue the rest. + +Example: + +```ini +; Need context: is this shown during combat or menu? +msg.abcdef123456=Original English text +``` + +## Submitting Your Translation + +Share these files: +1. Your language file, for example `lang/es.ini`. +2. Optional notes for unclear lines. + +That is all you need to contribute a translation.