From 671d6b78983649e9b610e6d5ace961745a97d1a0 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Sat, 28 Feb 2026 01:03:26 -0500 Subject: [PATCH] Translation updates and submodule sync --- .clang-format | 18 + .gitignore | 7 + AGENTS.md | 218 --- bugs.txt | 5 + docs/localization.md | 71 + docs/patterns.md | 278 ++++ docs/rune_system.md | 139 ++ draugnorak.nvgt | 254 +-- excluded_sounds.nvgt | 87 -- files/instructions.md | 4 + fix_mac.sh | 42 + lang/af.ini | 969 ++++++++++++ lang/en.ini | 1372 +++++++++++++++++ lang/en.template.ini | 1372 +++++++++++++++++ lang/es.ini | 969 ++++++++++++ libstorm-nvgt | 2 +- minigame.nvgt | 77 + scripts/audit_untranslated_strings.py | 401 +++++ scripts/format-nvgt.sh | 76 + scripts/generate_i18n_catalog.py | 1200 ++++++++++++++ scripts/i18n_audit_allowlist.txt | 2 + scripts/validate_i18n_catalog.py | 152 ++ 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/dialog.ogg | 3 + sounds/enemies/draugrhaugr1.ogg | 3 + sounds/menu/menu_move.ogg | 4 +- sounds/menu/menu_select.ogg | 4 +- sounds/terrain/draugrhaugr_step.ogg | 3 + src/audio_utils.nvgt | 135 +- src/base_system.nvgt | 494 +++--- src/bosses/adventure_combat.nvgt | 51 +- src/bosses/adventure_system.nvgt | 40 +- src/bosses/bandit_hideout.nvgt | 234 ++- src/bosses/unicorn/unicorn_boss.nvgt | 167 +- src/combat.nvgt | 102 +- src/constants.nvgt | 79 +- src/crafting/craft_barricade.nvgt | 135 +- src/crafting/craft_buildings.nvgt | 184 ++- src/crafting/craft_clothing.nvgt | 303 ++-- src/crafting/craft_materials.nvgt | 291 ++-- src/crafting/craft_runes.nvgt | 274 +++- src/crafting/craft_tools.nvgt | 338 ++-- src/crafting/craft_weapons.nvgt | 193 ++- src/crafting/crafting_core.nvgt | 174 ++- src/creature_audio.nvgt | 32 +- src/enemies/bandit.nvgt | 130 +- src/enemies/flying_creatures.nvgt | 120 +- src/enemies/ground_game.nvgt | 103 +- src/enemies/undead.nvgt | 216 ++- src/environment.nvgt | 346 +++-- src/fishing.nvgt | 142 +- src/flying_creature_template.nvgt | 2 +- src/fylgja_system.nvgt | 113 +- src/i18n.nvgt | 778 ++++++++++ src/intro.nvgt | 84 + src/inventory_items.nvgt | 375 +++-- src/inventory_menus.nvgt | 2 +- src/item_registry.nvgt | 160 +- src/learn_sounds.nvgt | 12 +- src/menus/action_menu.nvgt | 164 +- src/menus/altar_menu.nvgt | 74 +- src/menus/base_info.nvgt | 44 +- src/menus/character_info.nvgt | 207 ++- src/menus/equipment_menu.nvgt | 114 +- src/menus/inventory_core.nvgt | 86 +- src/menus/menu_utils.nvgt | 34 +- src/menus/storage_menu.nvgt | 165 +- src/notify_compat.nvgt | 5 +- src/pet_system.nvgt | 525 ++++--- src/player.nvgt | 46 +- src/quest_system.nvgt | 172 ++- src/quests/bat_invasion_game.nvgt | 21 +- src/quests/catch_the_boomerang_game.nvgt | 61 +- src/quests/enchanted_melody_game.nvgt | 46 +- src/quests/escape_from_hel_game.nvgt | 20 +- src/quests/skeletal_bard_game.nvgt | 50 +- src/runes/rune_data.nvgt | 97 +- src/runes/rune_effects.nvgt | 75 +- src/save_system.nvgt | 753 +++++---- src/sound_settings.nvgt | 49 + src/terrain_lookup.nvgt | 11 +- src/text_reader_aliases.nvgt | 6 +- src/time_system.nvgt | 355 +++-- src/weapon_range_audio.nvgt | 34 +- src/weather.nvgt | 87 +- src/world/mountains.nvgt | 88 +- src/world/world_buildings.nvgt | 48 +- src/world/world_drops.nvgt | 60 +- src/world/world_fires.nvgt | 28 +- src/world/world_snares.nvgt | 38 +- src/world/world_streams.nvgt | 33 +- todo.txt | 72 + translate.md | 206 +++ 102 files changed, 14288 insertions(+), 3692 deletions(-) create mode 100644 .clang-format delete mode 100644 AGENTS.md 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 delete mode 100644 excluded_sounds.nvgt create mode 100755 fix_mac.sh 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 scripts/audit_untranslated_strings.py create mode 100755 scripts/format-nvgt.sh 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/dialog.ogg create mode 100644 sounds/enemies/draugrhaugr1.ogg create mode 100644 sounds/terrain/draugrhaugr_step.ogg create mode 100644 src/i18n.nvgt create mode 100644 src/intro.nvgt create mode 100644 src/sound_settings.nvgt create mode 100644 todo.txt create mode 100644 translate.md diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..ad19955 --- /dev/null +++ b/.clang-format @@ -0,0 +1,18 @@ +--- +BasedOnStyle: LLVM +IndentWidth: 4 +ContinuationIndentWidth: 4 +TabWidth: 4 +UseTab: Never +ColumnLimit: 120 +BreakBeforeBraces: Attach +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Empty +IndentCaseLabels: true +SortIncludes: Never +ReflowComments: false +PointerAlignment: Left +ReferenceAlignment: Left +SpaceBeforeParens: ControlStatements +... diff --git a/.gitignore b/.gitignore index 236e223..36ac0a9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ nvgt +*.apk +*.idsig +*.keystore *.zip lib/ lib_mac/ @@ -9,5 +12,9 @@ include/ *.bak *.wav *.opus +__pycache__/ +*.pyc .aider* +AGENTS.md README.html +package.sh diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 49632a2..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,218 +0,0 @@ -# Draugnorak Project Guidelines - -## Engine Reference - -- If unsure about NVGT behavior or API details, consult the engine source in `nvgt-git/`. - -## Asset Usage - -**CRITICAL**: The `bloodshed/` directory is for reference only. DO NOT use any assets from the bloodshed directory in this game. It exists solely to help with coding examples and understanding patterns, but none of its files should be included or referenced in the actual game code. - -## Project Structure - -- `src/` - Game source code modules - - `constants.nvgt` - Game configuration and constants - - `player.nvgt` - Player state and global timers - - `world_state.nvgt` - World object orchestration (legacy coordinator, modules are in `src/world/` and `src/enemies/`) - - `item_registry.nvgt` - Item definitions and inventory arrays - - `inventory_items.nvgt` - Equipment, quick slots, stack limits - - `inventory_menus.nvgt` - Menu orchestrator for inventory/storage/equipment/actions - - `crafting.nvgt` - Crafting orchestrator - - `quest_system.nvgt` - Quest queue + mini-game menu - - `bosses/adventure_system.nvgt` - Adventure menu + boss routing - - `inventory.nvgt` - Inventory system orchestrator (item registry, menus, crafting, runes) - - `environment.nvgt` - Trees, climbing, falling damage, rope climbing, and environmental interactions - - `combat.nvgt` - Combat system (spear/axe/sling) - - `time_system.nvgt` - Time, day/night, invasions, expansions, blessings, weather ticks - - `weather.nvgt` - Wind/rain/thunder ambience system - - `save_system.nvgt` - Save/load (AES encrypted, versioned) - - `base_system.nvgt` - Base automation (residents, food, defense, collection) - - `ui.nvgt` - UI helpers, terrain lookup - - `audio_utils.nvgt` - Audio helper functions - - `creature_audio.nvgt` - Creature voice/footstep/attack audio helpers - - `notify.nvgt` - Notification system (queue + history) - - `speech_history.nvgt` - Screen reader message history (comma/period navigation) - - `text_reader.nvgt` - Accessible text reader window -- `src/world/` - World objects - - `world_fires.nvgt`, `world_snares.nvgt`, `world_drops.nvgt` - - `world_buildings.nvgt` - Firepit, Herb Garden, Storage, Pasture, Stable, Altar - - `world_streams.nvgt` - Streams + water audio - - `mountains.nvgt` - Mountain ranges, elevation, streams - - `barricade.nvgt` - Base barricade health and reinforcement -- `src/enemies/` - Enemies and creatures - - `undead.nvgt` (zombies), `bandit.nvgt` (invasions/wandering), `ground_game.nvgt` (boars), `flying_creatures.nvgt` (geese) -- `src/crafting/` - Crafting categories and recipes - - `crafting_core.nvgt`, `craft_weapons.nvgt`, `craft_tools.nvgt`, `craft_materials.nvgt`, `craft_clothing.nvgt`, `craft_buildings.nvgt`, `craft_barricade.nvgt`, `craft_runes.nvgt` -- `src/menus/` - Menu subsystems (inventory, storage, equipment, action, base info, altar, character info) -- `src/runes/` - Rune data + effects -- `src/quests/` - Quest mini-games -- `src/bosses/` - Adventure/boss encounters -- `sounds/` - Game audio assets -- `draugnorak.nvgt` - Main game file - -## Game Mechanics - -### Inventory Limits -- Personal inventory stacks are capped at 9 -- Skin pouches add +2 stack capacity, backpacks add +9 - -### Map Layout -- Base area: x 0-4 (wood terrain) -- Grass area: x 5-19 (grass terrain, contains trees) -- Gravel area: x 20-34 (gravel terrain, contains stones) - -### Healing System -- Base area provides passive healing when player health < max -- Without herb garden: 1 HP every 2.5 minutes (150000ms) -- With herb garden: 1 HP every 30 seconds (30000ms) -- Herb garden can only be built in base area (x <= 4) -- Only one herb garden allowed per base - -### Fire System -- Fires require a firepit to build -- New fires start with 12 minutes (720000ms) of fuel -- Warning at 30 seconds remaining -- Fire goes out when fuel depletes -- Fire sound plays within 3 tiles distance -- Jumping over fire prevents damage -- Feeding fire is done from the Action menu (A) - -### Snare System -- Snares become active when player moves away -- Check every hour for catching/escaping small game -- Catch chance starts at 5%, increases by 5% per hour (max 75%) -- Escape chance: 0% for first hour after catch, then increases by 2% per hour (max 95%) -- Snare sound plays within 2 tiles distance -- Residents can retrieve game from snares (daytime only, requires meat in storage) - -### Tree System -- Trees regenerate fully after ~5 minutes (minute-by-minute refill logic) -- Tree ambient sound plays within 4 tiles distance -- Trees only play sound when not chopped -- Climb down only when in a tree (Down arrow does nothing during jumps or when not in a tree) -- Trees have height; falling damage is applied when falling from height - -### Notification System -- Important events use `notify()` function (plays sounds/notify.ogg + speaks message) -- Last 10 notifications stored in history -- Navigation keys: - - `\` - Repeat most recent notification - - `]` - Next (newer) notification - - `[` (Shift+Comma) - Previous (older) notification -- Navigating history speaks message without notification sound -- Speech history (screen_reader_speak wrapper) uses `,` and `.` for previous/next message - -### Search System -- Hold Shift for 1 second to search -- Search completes after 1-second delay - -### Undead System -- At night, zombies spawn (5 max) and wander outside the base -- Zombies cannot enter the base while barricade health > 0 -- Zombies attack the barricade for 4-6 damage when they reach it; play `sounds/enemies/zombie_hit.ogg` -- Zombies vanish at daybreak -- Player can hit zombies with spear (1-tile range) or sling (8-tile range) - -### Barricade System -- Base starts with 100 barricade health, capped at 500 -- In base, press B to report barricade health -- Crafting menu includes Barricade category for reinforcement -- Reinforcement costs and health: 3 sticks (+10), 5 vines (+15), 1 log (+30), 5 stones (+20) -- Barricade health does not reset during gameplay - -### Mountains, Rope Climbing, Falling -- Mountain ranges can appear during area expansion with elevation-based terrain -- Steep slopes require a rope; the player is prompted to press Up/Down to climb -- Moving left/right during rope climb cancels and causes a fall -- Falling damage is applied beyond a safe height (10 feet) - -### World Expansion & Invasions -- After day 2, bandit invasions can trigger and expand the map to the east -- Expansion is either a 30-tile biome strip or a 60-tile mountain range -- Invasions last 1 hour and spawn bandits in the expanded area during daytime - -### Weather & Ambience -- Weather transitions between clear, windy, rainy, stormy with wind/rain/thunder audio -- Day/night ambience crossfades on hour changes - -### Inventory + Equipment -- Item definitions live in `src/item_registry.nvgt` (personal + storage inventories) -- Equipment includes spear, axe, sling, bow, clothing, pouches, backpacks -- Combat logic currently supports spear/axe/sling; bow attacks are not implemented yet -- Quivers gate arrow capacity (12 arrows per quiver) -- Quick slots (keys 1-0) bind equipment from the Equipment menu - -### Buildings -- Firepit, Fire, Herb Garden, Storage, Pasture, Stable, Altar -- Storage enables base inventory menus and increases resident recruitment chance - -### Residents and Base Automation -- Residents consume meat daily, can repair the barricade, defend with stored weapons, and collect resources -- Residents use spears or slings (requires stones) for defense -- Daily weapon breakage occurs based on resident count - -### Favor, Altars, Incense, Blessings -- Altar sacrifices (S key, base only) convert items to Favor -- Burning incense (A key action) grants Favor per hour while active -- Blessings can trigger (heal, speed boost, barricade repair) and consume Favor - -### Quests (Mini-Games) -- Quest menu (Q, base only) appears after altar/favor requirements -- Max 4 active quests; rewards grant Favor and resources - -### Adventures & Bosses -- Adventure menu (Tab) available outside base; limited to once per day -- Mountain adventure: Unicorn boss; victory grants Favor and unlocks Rune of Swiftness - -### Runes -- Rune system in `src/runes/` (data + effects) -- Runes are crafted in Crafting > Runes after unlock (requires knife + clay + favor) -- Rune of Swiftness grants move speed and gathering bonuses; runed items are tracked separately - -## Menu Structure - -### Crafting Menu (C key, base only) -Organized into categories: -- **Weapons**: Spear, Sling -- **Tools**: Knife, Snare, Stone Axe, Fishing Pole, Rope, Reed Basket, Clay Pot -- **Materials**: Butcher Game, Incense -- **Clothing**: Skin gear, Moccasins, Pouch, Backpack -- **Buildings**: Firepit, Fire, Herb Garden, Storage, Pasture, Stable, Altar -- **Barricade**: Reinforcement options -- **Runes**: Available after rune unlocks - -### Equipment Menu (E key) -- Only shows items player actually has -- Shows equipped status -- Says "Nothing to equip" if inventory is empty - -### Inventory Menu (I key) -- Base + storage built: root menu for personal vs storage -- No storage: personal inventory only - -### Action Menu (A key) -- Place snares, feed fires, burn incense (context-sensitive) -- Tab in menu performs “max” action for the selected option - -### Base Info (B key) -- Barricade health, residents, storage totals, base buildings - -### Quest Menu (Q key) -- Base-only quest selection for mini-games - -### Adventure Menu (Tab key) -- Terrain-based adventures outside the base (once per day) - -## Testing - -- Always run `./nvgt -c draugnorak.nvgt` after code changes -- This compiles without opening the game window and prevents it from taking over the terminal - -## Code Standards - -- Use `notify()` for important game events that should be reviewable -- Use `screen_reader_speak()` for immediate feedback that doesn't need to be stored -- All menus must call `menu_background_tick()` each loop to keep game state updating -- **Time references**: When discussing time (hours, minutes, days), assume game time unless explicitly stated as "real time". Game time runs at 60x speed: 1 real minute = 1 game hour, 24 real minutes = 1 game day. -- See `docs/patterns.md` for module and creature patterns -- See `docs/rune_system.md` for rune data/effects/save requirements 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..10a43de --- 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" @@ -44,6 +45,7 @@ sound_pool p(300); #include "src/text_reader_aliases.nvgt" #include "src/notify_compat.nvgt" #include "src/learn_sounds.nvgt" +#include "src/intro.nvgt" #include "src/bosses/adventure_system.nvgt" const string MAIN_MENU_MUSIC_TRACK = "sounds/menu/Draugnorak.ogg; loop; f=1"; @@ -53,8 +55,9 @@ music_manager mainMenuMusic; const int MAIN_MENU_NEW_GAME = 0; const int MAIN_MENU_LOAD_GAME = 1; -const int MAIN_MENU_LEARN_SOUNDS = 2; -const int MAIN_MENU_EXIT = 3; +const int MAIN_MENU_STORY = 2; +const int MAIN_MENU_LEARN_SOUNDS = 3; +const int MAIN_MENU_EXIT = 4; void start_main_menu_music() { menu_music_resume_or_play(mainMenuMusic, MAIN_MENU_MUSIC_TRACK, MAIN_MENU_MUSIC_FADE_IN_MS); @@ -66,16 +69,16 @@ void stop_main_menu_music() { int run_main_menu() { start_main_menu_music(); - speak_with_history("Draugnorak. Main menu.", true); + speak_with_history(tr("system.main_menu.prompt"), 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 +88,12 @@ 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(tr("system.menu.main.story")); + entry_paths.insert_last(""); + entry_types.insert_last(0); + action_ids.insert_last(MAIN_MENU_STORY); + + 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 +109,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 +117,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 +125,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 +160,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(); @@ -174,11 +187,11 @@ void run_game() continue; } start_new_game(); - speak_with_history("New game started.", true); + speak_with_history(tr("system.game.new_game_started"), true); game_started = true; } else if (selection == MAIN_MENU_LOAD_GAME) { if (!has_save_game()) { - ui_info_box("Draugnorak", "Load Game", "No saves found."); + ui_info_box(tr("system.ui.window_title"), i18n_text("Load Game"), tr("system.load_game.no_saves_found")); continue; } string selectedFile; @@ -186,13 +199,17 @@ void run_game() continue; } if (load_game_state_from_file(selectedFile)) { - speak_with_history("Game loaded.", true); + speak_with_history(tr("system.game.loaded"), true); game_started = true; } else { string message = last_save_error; - if (message == "") message = "Unable to load save."; - ui_info_box("Draugnorak", "Load Game", message); + if (message == "") + message = tr("system.load_game.unable_to_load"); + ui_info_box(tr("system.ui.window_title"), i18n_text("Load Game"), message); } + } else if (selection == MAIN_MENU_STORY) { + run_story_dialog(); + continue; } else if (selection == MAIN_MENU_LEARN_SOUNDS) { run_learn_sounds_menu(); continue; @@ -201,11 +218,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,23 +230,22 @@ 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(); - speak_with_history("Paused. Press backspace to resume.", true); + speak_with_history(tr("system.game.paused_prompt"), true); } else { p.resume_all(); // Restart all timers to prevent time from accumulating while paused restart_all_timers(); - speak_with_history("Resumed.", true); + speak_with_history(tr("system.game.resumed"), true); } continue; } - + // Skip all game logic while paused if (game_paused) { continue; @@ -237,15 +253,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("", tr("system.exit.confirm")); + if (really_exit == 1) { + return_to_main_menu_requested = true; + continue; + } } - } - + // Time & Environment updates update_time(); update_crossfade(); @@ -263,42 +278,47 @@ 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); + dictionary burningArgs; + burningArgs.set("health", player_health); + speak_with_history(trf("system.combat.burning", burningArgs), 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); + dictionary healthArgs; + healthArgs.set("health", player_health); + speak_with_history(trf("system.combat.health_status", healthArgs), true); } } - + // Death check if (player_health <= 0) { if (!try_consume_heal_scroll()) { - speak_with_history("You have died.", true); + speak_with_history(tr("system.combat.you_died"), true); wait(2000); return_to_main_menu_requested = true; continue; } } - + if (fylgjaCharging) { update_fylgja_charge(); continue; } - + // Inventory & Actions check_inventory_keys(x); check_action_menu(x); @@ -312,34 +332,41 @@ 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"; - string terrain = get_terrain_at_position(x); + string direction_label = (facing == 1) ? tr("system.direction.east") : tr("system.direction.west"); + string terrain = i18n_translate_fragment_value(get_terrain_at_position(x)); if (get_mountain_at(x) !is null) { - terrain += ". Mountains"; + dictionary terrainArgs; + terrainArgs.set("terrain", terrain); + terrain = trf("system.character.report.terrain_mountains", terrainArgs); } - speak_with_history(direction_label + ", x " + x + ", y " + y + ", terrain " + terrain, true); + dictionary positionArgs; + positionArgs.set("direction", direction_label); + positionArgs.set("x", x); + positionArgs.set("y", y); + positionArgs.set("terrain", terrain); + speak_with_history(trf("system.character.report.position", positionArgs), 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 +379,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 +396,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 +413,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,48 +422,47 @@ 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; - speak_with_history("west", true); + speak_with_history(tr("system.direction.west"), true); walktimer.restart(); // Cancel pending rope climb when changing direction pending_rope_climb_x = -1; } if (key_pressed(KEY_RIGHT) && facing != 1 && !climbing && !falling && !rope_climbing) { facing = 1; - speak_with_history("east", true); + speak_with_history(tr("system.direction.east"), true); walktimer.restart(); // 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 +474,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 +491,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 +515,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 +543,7 @@ void run_game() speak_ammo_blocked("No arrows."); } } - + if (bow_drawing && !ctrl_down) { release_bow_attack(x); bow_drawing = false; @@ -533,7 +552,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 +564,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 +579,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 deleted file mode 100644 index c2339fe..0000000 --- a/excluded_sounds.nvgt +++ /dev/null @@ -1,87 +0,0 @@ -// Project-local learn sounds configuration. -// Keep this file in the game root so libstorm-nvgt can stay untouched. - -void configure_project_learn_sounds() { - learn_sounds_reset_configuration(); - - // Skip entries can be full file paths or directories ending with "/". - learn_sounds_add_skip_entry("sounds/quests/bone1.ogg"); - learn_sounds_add_skip_entry("sounds/quests/bone2.ogg"); - learn_sounds_add_skip_entry("sounds/quests/bone3.ogg"); - learn_sounds_add_skip_entry("sounds/quests/bone4.ogg"); - learn_sounds_add_skip_entry("sounds/quests/bone5.ogg"); - learn_sounds_add_skip_entry("sounds/quests/bone6.ogg"); - learn_sounds_add_skip_entry("sounds/quests/bone7.ogg"); - learn_sounds_add_skip_entry("sounds/quests/bone8.ogg"); - learn_sounds_add_skip_entry("sounds/actions/fishpole.ogg"); - learn_sounds_add_skip_entry("sounds/actions/hit_ground.ogg"); - learn_sounds_add_skip_entry("sounds/menu/"); - learn_sounds_add_skip_entry("sounds/nature/"); - learn_sounds_add_skip_entry("sounds/pets/"); - learn_sounds_add_skip_entry("sounds/quests/"); - - 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." - ); - 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." - ); -} 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/fix_mac.sh b/fix_mac.sh new file mode 100755 index 0000000..7e9ec1d --- /dev/null +++ b/fix_mac.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +APP_PATH="/Applications/draugnorak.app" + +echo "Fixing Draugnorak at: $APP_PATH" + +############################################## +# 1. Remove Quarantine Attribute +############################################## +echo "Removing quarantine flag..." +xattr -dr com.apple.quarantine "$APP_PATH" 2>/dev/null + +############################################## +# 2. Ensure Executable Permissions +############################################## +echo "Ensuring executable permission..." +chmod +x "$APP_PATH/Contents/MacOS/"* 2>/dev/null + +############################################## +# 3. Detect Main Executable & Patch Info.plist +############################################## +EXECUTABLE=$(ls "$APP_PATH/Contents/MacOS/" | head -n 1) + +echo "Patching Info.plist..." +/usr/libexec/PlistBuddy -c "Set :CFBundleExecutable $EXECUTABLE" "$APP_PATH/Contents/Info.plist" 2>/dev/null +/usr/libexec/PlistBuddy -c "Set :CFBundlePackageType APPL" "$APP_PATH/Contents/Info.plist" 2>/dev/null +/usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier com.draugnorak.$EXECUTABLE" "$APP_PATH/Contents/Info.plist" 2>/dev/null + +############################################## +# 4. Ad-Hoc Code Re-sign +############################################## +echo "Re-signing bundle (ad-hoc)..." +sudo codesign --force --deep --sign - "$APP_PATH" + +############################################## +# 5. Refresh Launch Services +############################################## +echo "Refreshing LaunchServices database..." +/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -f "$APP_PATH" + +echo "Done!" +echo "Try launching Draugnorak normally." \ No newline at end of file diff --git a/lang/af.ini b/lang/af.ini new file mode 100644 index 0000000..702c6e6 --- /dev/null +++ b/lang/af.ini @@ -0,0 +1,969 @@ +[meta] +code=af +name=Afrikaans +native_name=Afrikaans + +[system] +sex.male=Manlik +sex.female=Vroulik +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. +load_game.save_deleted=Stoorlêer uitgevee. +load_game.no_saves_found=Geen stoorleers gevind nie. +load_game.unable_to_load=Kon nie stoorleer laai nie. +load_game.select_prompt=Laai speletjie. Kies karakter. Druk Delete om'n stoorlêer te verwyder. +main_menu.prompt=Draugnorak. Hoofkieslys. +game.new_game_started=Nuwe speletjie begin. +game.loaded=Speletjie gelaai. +game.paused_prompt=Gepouseer. Druk backspace om te hervat. +game.resumed=Hervat. +exit.confirm=Wil jy regtig afsluit? +direction.west=wes +direction.east=oos +inventory.cant_carry_any_more_item=Jy kan nie meer {item} dra nie. +menu.main.story=Die storie +story.voice.format={speaker}: {line} +story.god.fallback='n god +story.god.odin=Odin +story.god.thor=Thor +story.god.freyja=Freyja +story.god.loki=Loki +story.god.tyr=Tyr +story.god.baldur=Baldur +story.god.frigg=Frigg +story.god.heimdall=Heimdall +story.god.hel=Hel +story.god.fenrir=Fenrir +story.god.freyr=Freyr +story.line01=Die wereld het geval. +story.line02=Eens groot stede le in puin. +story.line03=Op die meeste plekke bly nie twee klippe oor soos die mensdom dit geplaas het nie. +story.line04=Die gode, lank as slapend beskou, het die onmoontlike reggekry. +story.line05=Ragnarok is afgeweer. +story.line06=Groot Fenrir is bevry en het nie sy gevangenemers neergevel nie. +story.line07=Surtr het nie ons wereld met sy vlammende grootswaard verwoes nie. +story.line08=Vir 'n vlugtige era het die gode die noodlot tot stilte gedwing. +story.line09=Maar die einde van die wereld is net vertraag, nooit ontken nie. +story.line10=Een mag was selfs buite goddelike bevel: die mensdom. +story.line11=Haat het geswel. +story.line12=Hebsug het geantwoord. +story.line13=Barmhartigheid het verdor. +story.line14=Verskille het misdade geword. +story.line15=Mense het nie meer geredeneer nie; hulle het mekaar soos diere verskeur. +story.line16=Bure het vyande geword oor nietighede: 'n kleur, 'n lied, 'n enkele woord. +story.line17=Oorlog het gevolg. +story.line18=Stede het gebrand, paaie het gebreek, en monsters met menslike gesigte het deur die strate gedwaal. +story.line19=Toe het verwoesting in terreur verander. +story.line20=Die dooies, woedend oor sinnelose slagting, het uit hul grafte opgestaan. +story.line21=Lyke het wakker geword en die lewendes gejag. +story.line22=Nagjagters het op bloed gevoed. +story.line23=Selfs as en stof het hul vorm onthou en weer opgestaan. +story.line24=Kort voor lank was al wat oorbly net puin en been. +story.line25=Jou laaste herinnering is 'n vlug voor 'n enorme horde nagmerriewesens. +story.line26=Wanneer jy wakker word, is jy iewers vreemd. +story.line27='n Growwe houthut staan teen die wind, oop vir die elemente, gestut met stokke en stamme. +story.line28=Wanneer jou sintuie terugkeer, roer dowwe stemme in die donker. +story.line29=Na die dood van hierdie wereld sal 'n nuwe een opstaan. +story.line30=Jy is waardig geoordeel om te probeer oorleef. +story.line31=Hierdie wereld behoort aan die dooies. +story.line32=Mettertyd sal hulle selfs ons doodmaak. +story.line33=Bewys jouself, kryger, en jy mag die volgende wereld binnegaan. +story.line34=Mag jou dood, wanneer dit kom, eerbaar wees. +story.line35=Ons sal help terwyl ons nog krag het, maar tyd is teen ons. +story.line36=Anderkant jou skuiling le grasveld, en verder, gebreekte klip wat eens 'n stad kon wees. +story.line37=In totale verwoesting het selfs herinnering sy vorm verloor. +language.select_prompt=Kies jou taal. +language.selected=Taal gestel op {language}. +language.english_label=English (en) +ui.window_title=Draugnorak +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? +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.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. +inventory.max_arrows_with_quivers=Jy kan net {max} pyle dra met jou huidige kokers. +inventory.item_not_available=Item nie beskikbaar nie. +search.found_item={item} gevind. +search.found_nothing=Niks gevind nie. +pickup.item={item} opgetel. +time.period.am=vm +time.period.pm=nm +time.report={hour} uur {period} dag {day} +time.report_with_conditions={time}. {conditions} +time.condition.sunny=Sonnig +time.condition.daylight=Daglig +time.condition.dark=Donker +time.condition.windy=Winderig +time.condition.raining=Reen +time.condition.storming=Stormagtig +time.condition.moon_and_stars=Maan en sterre +time.event.area_expanded=Die area het uitgebrei! Nuwe gebied is in die ooste ontdek. +time.event.mountain_discovered='n Bergreeks is in die ooste ontdek! +time.event.invasion_source_mountains=die berge +time.event.invasion_source_new_area=die nuwe area +time.event.invasion_start={enemy_plural} val in vanaf {source}! +time.event.zombie_swarm_spotted='n Swerm zombies is gewaar. +time.event.survivor_joins_one='n Oorlewende sluit by jou basis aan. +time.event.survivor_joins_many={count} oorlewendes sluit by jou basis aan. +time.event.base_max_capacity=Jou basis is op maksimum kapasiteit. +time.event.invasion_ended=Die {enemy} inval is verby. +time.event.blessing_speed_fades=Die spoedseening vervaag. +time.event.blessing_resident_fades=Die inwoners se doelgerigtheid vervaag. +time.event.blessing_search_fades=Die arend se sig vervaag. +time.event.sun_setting=Die son sak. +time.event.sky_brightening=Die lug begin lig word. +time.event.residents_repaired_barricade=Inwoners het die barrikade herstel. +{health} gesondheid. +time.event.incense_burned_out=Die wierook het uitgebrand. +combat.burning=Brand! {health} gesondheid oor. +combat.health_status={health} gesondheid. +combat.you_died=Jy het gesterf. +enemies.residents_killed_attacker=Inwoners het 'n aanvallende {enemy} doodgemaak. +enemies.resident_taken='n Inwoner is weggevoer. +action.fire.intensity.giant_blaze=Groot vlam +action.fire.intensity.small_fire=Klein vuur +action.fire.intensity.glowing_coals='n Paar gloeiende kole +action.fire.estimate.less_than_one_hour=minder as 1 uur +action.fire.estimate.approximately_one_hour=ongeveer 1 uur +action.fire.estimate.approximately_hours=ongeveer {hours} uur +action.fire.status={label}. Nog {estimate} oor. +action.no_fire_nearby=Geen vuur naby nie. +action.no_snare_nearby=Geen strik naby nie. +action.snare_holds=Strik hou 'n {catch_type}. +action.snare_set_inactive=Strik is gestel maar nog nie aktief nie. +action.snare_set_empty=Strik is gestel en leeg. +action.fish_on_line=Vis aan die lyn. +action.no_fish_on_line=Geen vis aan die lyn nie. +action.cannot_place_snares_in_base=Kan nie strikke in die basisarea plaas nie. +action.snare_already_here=Daar is reeds 'n strik hier. +action.snare_set=Strik gestel. +action.no_snares_to_place=Geen strikke om te plaas nie. +action.feed_fire.stick=Jy gooi 'n armvol stokke in die vuur. +action.feed_fire.vine=Jy gooi 'n paar rankplante en blare in die vuur. +action.feed_fire.log=Jy gooi 'n stam in die vuur. +action.no_altar_built=Geen altaar gebou nie. +action.need_clay_pot_for_incense=Jy het 'n kleipot nodig om wierook te brand. +action.no_incense_to_burn=Geen wierook om te brand nie. +action.incense_burning=Wierook brand. {hours} uur oor. +action.no_sticks_to_feed_fire=Geen stokke om die vuur te voed nie. +action.dumped_sticks={amount} stokke in die vuur gegooi. +action.no_vines_to_feed_fire=Geen rankplante om die vuur te voed nie. +action.dumped_vines={amount} rankplante in die vuur gegooi. +action.no_logs_to_feed_fire=Geen stamme om die vuur te voed nie. +action.dumped_logs={amount} stamme in die vuur gegooi. +action.burned_incense={amount} wierook gebrand. +{hours} uur. +action.option.place_snare=Plaas strik +action.option.feed_fire_stick=Voer vuur met stok +action.option.feed_fire_vine=Voer vuur met rankplant +action.option.feed_fire_log=Voer vuur met stam +action.option.burn_incense=Brand wierook +action.option.check_fire=Kontroleer vuur +action.option.check_snare=Kontroleer strik +action.option.check_fishing_pole=Kontroleer visstok +action.menu.prompt=Aksiekieslys. {option} +action.cant_do_that=Kan dit nie doen nie. +base_info.not_in_base=Jy is nie in die basis nie. +base_info.menu.no_options=Basisinligting. Geen opsies nie. +base_info.menu.prompt=Basisinligting. {option} +base.barricade_fallen=Die barrikade het geval! +base.no_food_residents_hungry=Geen kos nie, inwoners is honger. +base.resident_fishing_pole_broke_one='n Inwoner se visstok het gebreek. +base.resident_fishing_pole_broke_many={count} visstokke het gebreek. +base.resident_caught_fish_one='n Inwoner het 'n vis gevang en dit by berging gevoeg. +base.resident_caught_fish_many=Inwoners het {count} vis gevang en dit by berging gevoeg. +base.resident_smoked_fish='n Inwoner het 'n vis gerook in {yield} gerookte vis. +base.livestock_produced=Vee het {meat} vleis, {skins} velle en {feathers} vere geproduseer en by berging gevoeg. +base.resident_spear_broke_one='n Inwoner se speer het van slytasie gebreek. +base.resident_spear_broke_many={count} spere het van slytasie gebreek. +base.resident_sling_broke_one='n Inwoner se slinger het van slytasie gebreek. +base.resident_sling_broke_many={count} slingers het van slytasie gebreek. +base.resident_bow_broke_one='n Inwoner se boog het van slytasie gebreek. +base.resident_bow_broke_many={count} boe het van slytasie gebreek. +base.resident_clothing_repair_one='n Inwoner herstel klere. Gebruik {vines} rankplante, {skins} velle en {down} dons. +base.resident_clothing_repair_many=Inwoners herstel klere. Gebruik {vines} rankplante, {skins} velle en {down} dons. +base.snare_escape_during_check='n {catch_type} het ontsnap terwyl 'n inwoner die strik by x {position} nagegaan het. +base.resident_retrieved_from_snare='n Inwoner het {game_type} uit strik by x {position} y 0 gehaal en dit teruggestel. +base.resident_knife_broke_butchering='n Inwoner se mes het gebreek tydens slag. +base.resident_butchered_result='n Inwoner het {game_type} geslag. Het {meat} vleis, {skins} velle, {feathers} vere, {down} dons, {sinew} sening by berging gevoeg. +base.resident_added_to_storage='n Inwoner het {item} by berging gevoeg. +base.resident_basket_broke_one='n Inwoner se mandjie het gebreek. +base.resident_basket_broke_many={count} mandjies het gebreek. +base.resident_basket_broke_foraging_one='n Inwoner se mandjie het gebreek tydens kossoek. +base.resident_basket_broke_foraging_many={count} mandjies het gebreek tydens kossoek. +base.resident_gathered_basket_food_one='n Inwoner het 'n mandjie vrugte en neute versamel. +base.resident_gathered_basket_food_many=Inwoners het {count} mandjies vrugte en neute versamel. +fishing.size.small=klein +fishing.size.medium=mediumgroot +fishing.size.large=groot +fishing.size.monster=monster +fishing.pole_broke.default=Jou visstok het gebreek. +fishing.pole_broke.switched_weapons=Jy het wapens gewissel en jou visstok het gebreek. +fishing.pole_broke.moved_fish_got_away=Jy het beweeg en die vis het weggekom. Jou visstok het gebreek. +fishing.pole_broke.moved=Jy het beweeg en jou visstok het gebreek. +fishing.require_pole_equipped=Jy moet 'n visstok toegerus hê. +fishing.require_near_stream=Jy moet binne 2 teëls van 'n stroom wees. +fishing.fish_on_line='n Vis is aan die lyn! +fishing.caught='n {size} {fish} gevang. +fishing.fish_slipped_off_water=Die vis het in die water afgegly. +fishing.fish_got_away=Die vis het weggekom. +fishing.fish_still_on_line=Die vis is steeds aan die lyn. +fishing.stopped=Jy stop met visvang. +altar.must_be_in_base=Moet in die basis wees om die altaar te gebruik. +altar.no_altar_built=Geen altaar gebou nie. +altar.runed_cannot_sacrifice=Geruinde items kan nie geoffer word nie. +altar.nothing_to_sacrifice=Niks om te offer nie. +altar.sacrificed_one=Het 1 {item} geoffer. Guns +{gain}. Totaal {total}. +altar.sacrificed_many=Het {count} {items} geoffer. Guns +{gain}. Totaal {total}. +altar.menu.prompt=Altaar. Guns {favor}. +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. +storage.runed_item_name=Geruinde {equipment} van {rune} +storage.deposited_one=Gedeponeer {name}. +storage.deposited_many=Gedeponeer {amount} {item}. +storage.withdrew_one=Onttrek {name}. +storage.withdrew_many=Onttrek {amount} {item}. +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. +character.info.name_sex=Naam {name}, {sex} +character.info.name_unknown=Naam onbekend +character.info.health=Gesondheid {health} van {max} +character.info.weapon=Wapen {weapon} +character.info.clothing_equipped=Klere toegerus: {items} +character.info.no_clothing=Geen klere toegerus nie. +character.info.missing=Ontbreek: {slots} +character.info.favor=Guns {favor} +character.info.speed=Spoed {status} +character.info.pet=Troeteldier {pet}, gesondheid {health} van {max_health}, lojaliteit {loyalty} van {max_loyalty} +character.info.pet_knocked_out=Bewusteloos vir {hours} {hour_label}. +character.info.pet_none=Geen troeteldier nie. +character.word.hour=uur +character.word.hours=ure +pet.default_name=troeteldier +pet.returned='n {pet} keer terug na jou. +pet.knocked_out=Jou {pet} is uitgeslaan. +pet.knocked_out_with_remaining=Jou {pet} is uitgeslaan. {hours} {hour_label} oor. +pet.leaves={pet} gaan weg. +pet.leaving=Jou {pet} is besig om weg te gaan. +pet.on_the_way=Jou troeteldier is op pad. +pet.hungry_unresponsive='n {pet} is honger en reageer nie. +pet.joins_you='n {pet} sluit by jou aan. +pet.offer.prompt='n Vriendelike {pet} smeek vir kos. Aanvaar? +pet.offer.declined=Afgewys. +pet.retrieve.cant_carry_chew_on=Jy kan nie 'n {item} dra nie, so jy gee dit vir 'n {pet} om aan te kou. +pet.retrieve.retrieved_item=Jou {pet} het {item} gebring. +pet.retrieve.random_find=Jou {pet} het {count} {item} gebring. +pet.recovered_from_injuries=Jou {pet} het van sy beserings herstel. +pet.getting_hungry='n {pet} begin honger word. +fylgja.already_used_today=Jy het reeds vandag jou Fylgja gebruik. +fylgja.menu.prompt=Fylgja-kieslys. +fylgja.need_open_ground_to_charge=Jy het oop grond voor jou nodig om te storm. +fylgja.option={name} Fylgja +fylgja.connection_with_target=Jy het 'n {stage}-verbintenis met die {target}. +fylgja.unlocked=Jy het die {name} Fylgja ontsluit! +fylgja.already_unlocked=Jy het reeds die {name} Fylgja ontsluit. +fylgja.name.unicorn=Eenhoring +fylgja.target.unicorn=eenhoring +fylgja.stage.tenuous=broos +fylgja.stage.faint=flou +fylgja.stage.stirring=ontwakend +fylgja.stage.budding=ontluikend +fylgja.stage.kindled=aangewakkerd +fylgja.stage.bound=gebonde +fylgja.stage.sworn=geswore +fylgja.stage.ascendant=stygend +fylgja.stage.ultimate=uiteindelik +quest.name.bat_invasion=Vlermuis-inval +quest.name.catch_boomerang=Vang die boemerang +quest.name.enchanted_melody=Betowerde melodie +quest.name.escape_from_hel=Ontsnap uit Hel +quest.name.skeletal_bard=Skelet-bard +quest.name.unknown=Onbekende soeke +quest.description.bat_invasion=Vlermuis-inval. Reuse moordvlermuise val aan. Druk spasie om te gooi wanneer die vlermuis gesentreer klink. +quest.description.catch_boomerang=Vang die boemerang. Gooi en vang die boemerang op sy terugkeer vir tot 4 punte. +quest.description.enchanted_melody=Betowerde melodie. Herhaal die patroon met E R D F of U I J K. Laagste na hoogste toon. +quest.description.escape_from_hel=Ontsnap uit Hel. Druk spasie om oor oop grafte te spring. Die tempo versnel. +quest.description.skeletal_bard=Skelet-bard. 'n Skelet genaamd Billy Bones oefen om 'n bard te word. Tel die note. +quest.description.unknown=Onbekende soeke. +quest.new_available='n Nuwe soeke is beskikbaar: {quest}. +quest.none_available=Geen soekes beskikbaar nie. +quest.favor_phrase.displeased=Die gode is ontevrede met jou prestasie. +quest.favor_phrase.pleased=Die gode is tevrede met jou prestasie. +quest.favor_phrase.very_pleased=Die gode is baie tevrede met jou prestasie. +quest.favor_phrase.extremely_pleased=Die gode is uiters tevrede met jou prestasie. +quest.favor_phrase.ultimately_pleased=Die gode is uitermate tevrede met jou prestasie. +quest.rewards.title=Soeke-belonings +quest.rewards.complete_heading=Soeke voltooi! +quest.rewards.heading=Belonings: +quest.rewards.favor_line=Guns: +{favor} +quest.rewards.item_line={item}: +{amount} +quest.rewards.inventory_full_line=Voorraad vol, kon nie {item} ontvang nie. +quest.rewards.no_items_awarded=Geen items toegeken nie. +quest.rewards.score_line=Telling: {score} +quest.menu.prompt=Soeke-kieslys. +quest.menu.count={index} van {total} +quest.minigame.starting=Begin. +quest.bat_invasion.complete_score=Vlermuis-inval voltooi. Telling {score}. +quest.boomerang.press_space_to_throw=Druk Spasie om te gooi. +quest.boomerang.skill.almost_dropped=byna laat val +quest.boomerang.skill.managed_to_catch=net-net gevang +quest.boomerang.skill.caught=gevang +quest.boomerang.skill.expertly_caught=meesterlik gevang +quest.boomerang.caught_result=Jy het die boemerang {skill}. {points} punte.{suffix} +quest.boomerang.missed=Jy het die boemerang gemis.{suffix} +quest.boomerang.hit_you=Die boemerang het jou getref.{suffix} +quest.boomerang.summary=Jy het {count} boemerangs gevang vir 'n totaal van {score} punte. +quest.enchanted_melody.practice_prompt=Oefenmodus. Druk Enter om te begin, Escape om te kanselleer. +quest.enchanted_melody.starting_prompt=Begin. Herhaal die patroon. +quest.enchanted_melody.score=Jy het {rounds} note reg gekry. Telling {score}. +quest.skeletal_bard.select_notes=Kies aantal note. {selection}. +quest.skeletal_bard.round_result=Jy het {guess} note ingetik en die werklike getal was {actual}. {points} punte. Druk Enter om voort te gaan. +quest.skeletal_bard.complete_score=Skelet-bard voltooi. Jou telling vir al vyf wysies is {score}. +quest.escape_from_hel.fell_in_score=Jy het ingevall. Telling {score}. +adventure.none_in_base=Geen avonture beskikbaar in die basis nie. +adventure.already_attempted_today=Jy het reeds vandag 'n avontuur probeer. +adventure.option.unicorn_hunt=Eenhoringjag (Bergbaas) +adventure.option.bandit_hideout=Bandietskuilplek +adventure.none_in_area=Geen avonture in hierdie area gevind nie. +adventure.menu.prompt=Avontuur-kieslys. +adventure.victory=Oorwinning! +adventure.rewards.title==== Oorwinningsbelonings === +adventure.unicorn.flee=Jy het van die eenhoring weggevlug. +adventure.unicorn.position_report=X {x}, {terrain}, kyk {direction} +adventure.unicorn.player_trampled=Die eenhoring het jou vertrap! +adventure.unicorn.victory_title=Eenhoringoorwinning +adventure.unicorn.reward.favor_awarded=Die gode is tevrede met jou oorwinning! {favor} guns toegeken. +adventure.unicorn.reward.heal_scrolls=Genesingsrolle: +{count}. +adventure.unicorn.reward.learned_rune_swiftness=Rune van Snelheid geleer! +adventure.unicorn.reward.engrave_unlocked=Jy kan nou toerusting met hierdie rune graveer by die handwerkkieslys. +adventure.unicorn.reward.mastered_rune_swiftness=Jy het reeds die Rune van Snelheid bemeester. +adventure.unicorn.horses.no_stable_or_storage=Stal of berging nie gebou nie. Geen perde is gevang nie. +adventure.unicorn.horses.stable_full=Stal is vol. Geen perde is gevang nie. +adventure.unicorn.horses.joined_stable='n Perd sluit by jou stal aan. +adventure.unicorn.horses.none_captured=Geen perde is gevang nie. +adventure.bandit_hideout.flee=Jy het van die bandietskuilplek weggevlug. +adventure.bandit_hideout.position_report=X {x}, {terrain}. Afstand na basis {distance}. Kyk {direction}. Basisgesondheid {health} van {max}. +adventure.bandit_hideout.player_defeated=Jy is in die bandietskuilplek verslaan. +character.report.position={direction}, x {x}, y {y}, terrein {terrain} +character.report.terrain_mountains={terrain}. Berge +character.report.player={health} van {max} gesondheid +character.report.pet={pet}: {health} van {max} gesondheid, lojaliteit {loyalty} van {max_loyalty} +item.label.items=voorwerpe +item.label.item=voorwerp +item.label.unknown=Onbekend +equipment.menu.prompt=Toerustingkieslys. {option} +equipment.menu.nothing_to_equip=Niks om toe te rus nie. +equipment.menu.equipped_suffix=(toegerus) +equipment.set_to_slot={name} aan gleuf {slot} toegeken. +equipment.equipped={name} toegerus. +equipment.unequipped={name} onttoegerus. +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. +environment.fall.damage_report=Het {height} voet geval! {damage} skade geneem. {health} gesondheid oor. +environment.snare.collected_with_catch={catch_type} en strik versamel. +environment.snare.collected_empty=Strik versamel. +environment.tree.cut_down=Hierdie boom is afgekap. +environment.tree.empty=Hierdie boom is leeg. +environment.area.nothing_left=Hierdie area het niks oor nie. +environment.tree.climb_started=Begin boom klim. Hoogte is {height} voet. +environment.tree.climb_reached_top=Top bereik op {height} voet. +environment.tree.climb_reached_ground=Veilig die grond bereik. +environment.tree.climb_down=Besig om af te klim. +environment.movement.need_rope=Jy sal 'n tou nodig hê om daar te klim. +environment.movement.press_up_climb=Druk op om op te klim. +environment.movement.press_down_climb=Druk af om af te klim. +environment.movement.need_canoe=Jy het 'n kano nodig om diep water oor te steek. +environment.rope_climb.direction.up=op +environment.rope_climb.direction.down=af +environment.rope_climb.progress=Klim {direction}. {distance} voet. +environment.rope_climb.reached_elevation=Hoogte {elevation} bereik. +snare.escape_at='n {catch_type} het uit jou strik by x {position} y 0 ontsnap! +snare.caught_at={catch_type} in strik gevang by x {position} y 0! +snare.stepped_on_with_escape=Jy het op jou strik getrap! Die {catch_type} het ontsnap. +snare.stepped_on_broke=Jy het op jou strik getrap en dit gebreek! +fire.low_fuel_at=Vuur by x {position} y {y} raak min brandstof! +fire.went_out_at=Vuur by x {position} y {y} het uitgegaan. +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.in_progress=Besig om te handwerk... +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.weapons.crafted.spear='n Speer gemaak. +crafting.weapons.crafted.sling='n Slinger gemaak. +crafting.weapons.crafted.bow='n Boog gemaak. +crafting.weapons.crafted.stone_axe='n Klipbyl gemaak. +crafting.weapons.crafted_max.spears={count} Spere gemaak. +crafting.weapons.crafted_max.slings={count} Slingers gemaak. +crafting.weapons.crafted_max.bows={count} Boe gemaak. +crafting.weapons.crafted_max.stone_axes={count} Klipbyle gemaak. +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.tools.crafted.stone_knife='n Klipmes gemaak. +crafting.tools.crafted.snare='n Strik gemaak. +crafting.tools.crafted.fishing_pole='n Visstok gemaak. +crafting.tools.crafted.rope=Tou gemaak. +crafting.tools.crafted.quiver='n Koker gemaak. +crafting.tools.crafted.canoe='n Kano gemaak. +crafting.tools.crafted.reed_basket='n Rietmandjie gemaak. +crafting.tools.crafted.clay_pot='n Kleipot gemaak. +crafting.tools.crafted_max.stone_knives={count} Klipmesse gemaak. +crafting.tools.crafted_max.snares={count} Strikke gemaak. +crafting.tools.crafted_max.fishing_poles={count} Visstokke gemaak. +crafting.tools.crafted_max.ropes={count} Toue gemaak. +crafting.tools.crafted_max.quivers={count} Kokers gemaak. +crafting.tools.crafted_max.canoes={count} Kano's gemaak. +crafting.tools.crafted_max.reed_baskets={count} Rietmandjies gemaak. +crafting.tools.crafted_max.clay_pots={count} Kleipotte gemaak. +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.materials.not_enough_quiver_capacity_for_arrows=Nie genoeg kokerruimte vir pyle nie. +crafting.materials.crafted.arrows={count} pyle gemaak. +crafting.materials.crafted.bowstring='n Boogstring gemaak. +crafting.materials.crafted_max.bowstrings={count} boogstringe gemaak. +crafting.materials.crafted.incense=Wierook gemaak. +crafting.materials.crafted_max.incense={count} wierook gemaak. +crafting.materials.smoke_fish.crafted=1 vis gerook in {yield} gerookte vis. +crafting.materials.smoke_fish.crafted_max={count} vis gerook in {yield} gerookte vis. +crafting.materials.butcher.goose=Gans geslag. Het 1 vleis, vere en dons gekry. +crafting.materials.butcher.turkey=Kalkoen geslag. Het 1 vleis en vere gekry. +crafting.materials.butcher.boar=Beerkarkas geslag. Het vleis, 3 velle en 2 sening gekry. +crafting.materials.butcher.default={game} geslag. Het 1 vleis en 1 vel gekry. +crafting.materials.butcher.no_space_outputs=Geen plek vir uitsette nie. +crafting.materials.butcher.max_result={count} wild geslag. Het {meat} vleis, {skins} velle, {feathers} vere, {down} dons, {sinew} sening gekry. +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.clothing.crafted.skin_hat='n Velhoed gemaak. +crafting.clothing.crafted.skin_gloves=Velhandskoene gemaak. +crafting.clothing.crafted.skin_pants=Velbroek gemaak. +crafting.clothing.crafted.skin_tunic='n Veltoonika gemaak. +crafting.clothing.crafted.moccasins=Mokkasiene gemaak. +crafting.clothing.crafted.skin_pouch='n Velsakkie gemaak. +crafting.clothing.crafted.backpack='n Rugsak gemaak. +crafting.clothing.crafted_max.skin_hats={count} Velhoede gemaak. +crafting.clothing.crafted_max.skin_gloves={count} Velhandskoene gemaak. +crafting.clothing.crafted_max.skin_pants={count} Velbroeke gemaak. +crafting.clothing.crafted_max.skin_tunics={count} Veltoonikas gemaak. +crafting.clothing.crafted_max.moccasins={count} Mokkasiene gemaak. +crafting.clothing.crafted_max.skin_pouches={count} Velsakkies gemaak. +crafting.clothing.crafted_max.backpacks={count} Rugsakke gemaak. +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.buildings.none_available=Geen geboue beskikbaar nie. +crafting.buildings.firepit.already_in_base=Daar is reeds 'n vuurherd in die basis. +crafting.buildings.firepit.already_here=Daar is reeds 'n vuurherd hier. +crafting.buildings.firepit.built=Vuurherd hier gebou. +crafting.buildings.fire.requires_firepit_nearby=Jy het 'n vuurherd binne 2 teëls nodig om 'n vuur te bou. +crafting.buildings.fire.already_in_base=Daar is reeds 'n vuur in die basis. +crafting.buildings.fire.built=Vuur by die vuurherd gebou. +crafting.buildings.herb_garden.base_only=Kruietuin kan slegs in die basisarea gebou word. +crafting.buildings.herb_garden.already_in_base=Daar is reeds 'n kruietuin in die basis. +crafting.buildings.herb_garden.built=Kruietuin gebou. Die basis genees nou vinniger. +crafting.buildings.storage.base_only=Berging moet in die basis gebou word. +crafting.buildings.storage.fully_upgraded=Berging is volledig opgegradeer. +crafting.buildings.storage.upgraded=Berging opgegradeer. Kapasiteit is nou {capacity} per item. +crafting.buildings.pasture.base_only=Weiveld moet in die basis gebou word. +crafting.buildings.pasture.requires_storage_upgrade=Berging moet opgegradeer word voor 'n weiveld. +crafting.buildings.pasture.already_built=Weiveld is reeds gebou. +crafting.buildings.pasture.built=Weiveld gebou. +crafting.buildings.stable.base_only=Stal moet in die basis gebou word. +crafting.buildings.stable.requires_storage_upgrade=Berging moet opgegradeer word voor 'n stal. +crafting.buildings.stable.already_built=Stal is reeds gebou. +crafting.buildings.stable.built=Stal gebou. +crafting.buildings.altar.base_only=Altaar moet in die basis gebou word. +crafting.buildings.altar.already_built=Altaar is reeds gebou. +crafting.buildings.altar.built=Altaar gebou. +crafting.runes.base_only=Rune-gravering kan net in die basisarea gedoen word. +crafting.runes.option={rune} (1 Klei, 1 Guns) [Vereis Mes] +crafting.runes.none_unlocked=Nog geen runes ontsluit nie. +crafting.runes.prompt=Runes. {option} +crafting.runes.equipment_option={name} ({count} beskikbaar) +crafting.runes.no_equipment_available=Geen toerusting beskikbaar om te graveer nie. +crafting.runes.select_equipment_prompt=Kies toerusting om met {rune} te graveer. {option} +crafting.runes.no_item_available=Geen {item} beskikbaar nie. +crafting.runes.runed_name=Geruinde {equipment} van {rune} +crafting.runes.engraved={item} gegraveer. +crafting.runes.engraved_max={count} {item} met {rune} gegraveer. +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.barricade.full_health=Die barrikade is reeds op volle gesondheid. +crafting.barricade.no_materials=Geen materiale om die barrikade te versterk nie. +crafting.barricade.reinforced.sticks=Barrikade versterk met stokke. +{gained} gesondheid. Nou {health} van {max}. +crafting.barricade.reinforced.vines=Barrikade versterk met rankplante. +{gained} gesondheid. Nou {health} van {max}. +crafting.barricade.reinforced.log=Barrikade versterk met stam. +{gained} gesondheid. Nou {health} van {max}. +crafting.barricade.reinforced.stones=Barrikade versterk met klippe. +{gained} gesondheid. Nou {health} van {max}. +crafting.barricade.reinforced_max.sticks=Barrikade {count} keer versterk met stokke. Gesondheid nou {health}. +crafting.barricade.reinforced_max.vines=Barrikade {count} keer versterk met rankplante. Gesondheid nou {health}. +crafting.barricade.reinforced_max.log=Barrikade {count} keer versterk met stam. Gesondheid nou {health}. +crafting.barricade.reinforced_max.stones=Barrikade {count} keer versterk met klippe. Gesondheid nou {health}. +crafting.barricade.not_enough.sticks=Nie genoeg stokke nie. +crafting.barricade.not_enough.vines=Nie genoeg rankplante nie. +crafting.barricade.not_enough.logs=Nie genoeg stamme nie. +crafting.barricade.not_enough.stones=Nie genoeg klippe nie. +crafting.missing=Ontbreek: {requirements} +crafting.requirement.stone_knife=Klipmes +crafting.requirement.game=Wild +crafting.requirement.favor=guns +crafting.requirement.resources=hulpbronne + +[messages] +msg.00884428322b=put +msg.01a159425acf=breek strik +msg.03268c7d7239=rugsakke +msg.03976920927e=tou +msg.0406529811c7=Klein Wild +msg.041d8413712d='n Ongeluklogboek is na jou stoordatalêer geskryf. +msg.04a8916a9494=vampier1 +msg.0532db2192fa=wierook +msg.066723a92e94==== Oorwinningsbelonings === +msg.07e9ab246cdd=Nadat jy hierdie skerm toegemaak het, kan jy die notas oefen. +msg.08b42842d5bc=sening +msg.096539f23463=Die oorlogstromme slaan! Berei voor vir geveg! +msg.09c28ae8f83b=Klippe +msg.09d016686864=Stal nie gebou nie +msg.0a3bdcef1906=Rietmandjies +msg.0a9d6438c818==== Betowerde Melodie === +msg.0b3d8d5cdca6=- Bereik die basis en vernietig die barrikade +msg.0bcd9af79f2d=woud +msg.0c7789cf9a52=handwerk voltooi +msg.0cf604cb001b=Pyle +msg.0d1ec2e04286=slinger swaai +msg.0e16a11cda9a=messe +msg.0e57fb642209=speer +msg.0eb796e65c39=Slingers +msg.0f9af12cbb82=velsakkie +msg.126dd3b70a5c=Stamme +msg.136e4cb03bd5=Kon nie lêer stoor nie +msg.145ba8a7a0f2=kokers +msg.14f5f5ff4f76=Geen oorlewendes gevind nie. +msg.16134a4250f7=wild val +msg.16a233f44489=- Luister vir oop grafte wat nader kom (word harder) +msg.176b9a0c8398=Wil jy regtig verlaat? +msg.17808e3775e1=wierookstokkie +msg.17b5b2a286cc==== Skelet-bard === +msg.18b837bb6f43=vlermuis1 +msg.18c77e824a2a=jeti sterf +msg.1a853cbfbc36=bandiet sterf +msg.1bb8ef2886a3=Basiese ondooie. +msg.1be2a44cb53d=nag +msg.1bfa6bc809d4==== Bandiete-skuilplek === +msg.1c7022965d1a=velhandskoen +msg.1d15d0d5c98d=Berging gebou. Totaal items {arg1} +msg.1d61343b9660=Berging nie gebou nie. Geen itembelonings nie. +msg.1da840244989=vuur +msg.1db76ac85135=slinger +msg.237b5017397b=byl +msg.245c89590526=- Wanneer die brug ineenstort met die Eenhoring daarop, wen jy! +msg.25db7a4ec692=Vee is herwin en by jou weiveld gevoeg. +msg.2630bd23d7d4=veilig om te spring +msg.2648db667f58=speer tref +msg.270dcc07d3ae=donderweer medium +msg.271e9a568c0e=spring +msg.2727056eb878=Weiveld gebou. Vee {arg1} van {arg2} +msg.273d131f0c8e=Geen kennisgewings nie. +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.29f291640516=Soekinligting +msg.2afb87ae1752=- Jy hardloop outomaties, spoed neem toe met tyd +msg.2bb11d7c39dc='n Vyand is binne bereik van jou huidige wapen. +msg.2bf7b1d59f34=eenhoring val +msg.2c6c80931cd1=Velhoede +msg.2e094dd0a2ab=speer swaai +msg.3030e8ec7633=Vis +msg.33bc40d8201e=- Na die deuntjie, kies die aantal notas +msg.33c89b4bdc4e=koker +msg.34daac1528e0=kieslysbeweging +msg.34fcedc354d9=visstok +msg.363b1d2abd57=Kos in berging {arg1} vleis, {arg2} gerookte vis, {arg3} mandjies vrugte en neute. Totaal {arg4}. Daaglikse verbruik {arg5} +msg.37423f3bd00f=velle +msg.37ef0562902c=rankplant +msg.381aea2617cb=Wierook +msg.38a7045d078c=donderweer laag +msg.38eb0fe3f78a=pyl vlieg +msg.39784ab5ac68=stukkie sening +msg.39a531d92bbe=pyl +msg.3a7d9767b123=voorwerp +msg.3c32451c9c71=velsakkies +msg.3cebb4c50866=visstok +msg.3e024b87e650=harde klip +msg.3e03229d39e4=Velle +msg.3eb9cd52ec8d=velhandskoene +msg.3f638a51cb47=E of U - Vierde nota (hoogste toonhoogte) +msg.4010ceb056dd=donderweer hoog +msg.403654dac805=been4 +msg.4037ea7ad431=spere +msg.408a612a4167=skis1 +msg.412fed81f58a=rietmandjie +msg.417705583821=wind middel +msg.41a682cadfe3=vlieg +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.437ecef821db=speer mis +msg.43821cae64c5=boogstrings +msg.444d4678962b=val +msg.4537c16badd9=boog skiet +msg.455b11a16eb7=strikke +msg.46fcdd91c99e=draugrhaugr stap +msg.474c797713f3=stamme +msg.483a17dd2a17=Strikke +msg.494f87e3874d=draugrhaugr1 +msg.495f723e9fa1=byle +msg.4a0e932d1bc5=gruis +msg.4a17d4fabe93=velbroeke +msg.4b9b4385b1a9=bumerang +msg.4c0d2469f78b=klippe +msg.4f08301dcb7b=kennisgewing +msg.4f11c6797b8d=vlak water +msg.4f773e9d5bf0=Boë +msg.4f7f358cd341=gans +msg.4f99d3d5b638=Kleipotte +msg.5009c8e190ee=kalkoen +msg.508e8bc0cc57=- Gebruik Op/Af om die getal te verander +msg.5123c22ce6a5=slinger lae reikafstand +msg.528fb1c4e5fb=Nuwe speletjie +msg.5460f2e3c43e=papdruk +msg.546330ee64db=visstok breek +msg.54fc2d657ed6=Draugnorak +msg.55748cf43121=vampier3 +msg.5665ef30ef4d=Vlermuise val aan! Gooi jou speer om te verdedig. +msg.584227d57290=Riette +msg.5906872f9bb6=vampier2 +msg.596350ae8387='n Riet gevind. +msg.599096c44c5b=Beerkarkasse +msg.5993f9da16fc=Leer geluide. +msg.5b61e4982b59=skis sterf +msg.5bdb7fa2223d=Vere +msg.5c2434ecd90f=riette +msg.5cac31311668=Weiveld is vol. Geen vee is herwin nie. +msg.5cc7429297c8=wat met 'n byl vernietig kan word. +msg.5dfc869b7588=gooi krag +msg.5ea84b1201bb={arg1} guns gee jou die oë van 'n arend. +msg.5f3f3a85393b=kieslys kies +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.62fac2f438bb=valk +msg.64875fcccaac=vis +msg.64b945d9072e=Kruietuin nie gebou nie +msg.65eb0fb6ab40=velbroek +msg.66fd9a7e91f8=rankplante +msg.67ae077e6458=Kruietuin gebou +msg.686021c25738=been3 +msg.6861fd95f4f7=- Daar is 5 beurte +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.704e634dd69f=tref grond +msg.714165bec103=stel strik +msg.717d7fcc68b7=- Daar is 5 rondtes, tot 4 punte elk +msg.71aad5f523eb=velhoede +msg.731152a028c6=Gesluit. +msg.7680f62917b1=- Druk weer Spasie wanneer dit naby klink +msg.77346d0447da=dons +msg.7748b68d141a=diep woud +msg.78f036ea9ebd=Die dier storm onvermoeid heen en weer. +msg.7921110eb7e1=besig met handwerk +msg.7968afd0c044=visstokke +msg.7babc233de26=stam +msg.7c49f713a0fb=pyle +msg.7c57f16628c7='n Klip gevind. +msg.7d2cf35450e3=Luister na die melodie en tel die notas. +msg.7d88f361f3e8=zombie1 +msg.7e83de91b4a2=bandietvrou sterf +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.81df04a81e24={arg1} guns skyn op jou. {arg2} +msg.823be948275f=- Beter tydsberekening verdien meer punte (tot 4) +msg.83865f248682=Kragtiger ondooie. Hulle verloor jou nie uit die oog wanneer jy in die basis is nie. +msg.863be329d064=dialoog +msg.8443de859917=wind hoog +msg.849a4f4a399d=Jy vlug uit die ryk van die dooies! +msg.85a177a79f1d=been2 +msg.85f15bc57d6c=veer +msg.862021235188=kano +msg.86290fe7e711=been5 +msg.86e257c3efcb=Oudste kennisgewing. {arg1} +msg.87383ce4344d=Boogstrings +msg.87cdb5c91437=toue +msg.8818f1d55166={arg1} glans vul inwoners met doelgerigtheid. +msg.88749dbbbf09==== Vlermuisinval === +msg.88a8bee5206e=- Lok die Eenhoring op die brug +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.917ee46db0cd=by +msg.924e0eb6b6f1=speler manlike skade +msg.9281d2fe5f08={arg1} guns skyn op jou. Jy voel vinnig vir 'n rukkie. +msg.92abc6d6953f=klim tou +msg.9379874d592b=Altaar nie gebou nie. Geen guns toegeken nie. +msg.93894d088f6a=- Gebruik jou byl om 'n brugstut te vernietig +msg.946466f6c963=stok +msg.957f554c342a=beer +msg.97c59037a63c='n Oorlewende sluit by jou basis aan. +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.9bbcfc6c8f9d=bandiet4 +msg.9c19f78d931d=been6 +msg.9dd40321998e=vlermuis2 +msg.a040b931b9bb=Hoe om te speel: +msg.a153dba5e67a=diep water +msg.a2620cbc10f5=dag +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.a6caf0c3771d=slingers +msg.a6e2492f34b6==== Ontsnap uit Hel === +msg.a79dc18e77d9=- Jy het 10 pogings +msg.a7cb7754af6c=Nuutste kennisgewing. {arg1} +msg.a95eab2b45da=- Spring (OP-pyltjie) om te verhoed dat jy vertrap word +msg.aa3a1916166d=goblin1 +msg.aa5edb7494f3=kleipot +msg.ab2244bc6027=genesingsrol +msg.abe73973cc7c=Kon nie stoorlêer laai nie. +msg.ac36af678d78=Kontroles: +msg.ad34f00c46cd=mokkasien +msg.ad36d64ad3a3=goblin sterf +msg.af93482d73f0=genesingsrolle +msg.aff5100bebc5=swart kat +msg.b0cca9f9205a=Stokke +msg.b0f6aa37f763=Rankplante +msg.b11aa393c8a8=Mokkasiene +msg.b31837f2fc5b=begin visvang +msg.b32fc272dc0d=- Luister vir vlermuise wat van links of regs verbyvlieg +msg.b48d0661f0da=Veltoonikas +msg.b5d5cc084a95=bandiet3 +msg.b73a0709297f=roep troeteldier +msg.b774102c89b5=Messe +msg.b94e9f3d7e00=sneeu +msg.ba3af68702a3=Rune van Vernietiging geleer! +msg.ba7d7dfe330b=Mandjies vrugte en neute +msg.bb0ea68db3b4=mandjie vrugte en neute +msg.bb40f75a9c60=vleis +msg.bbc29b76c59b=gras +msg.bdebda25dad9=pyl tref +msg.bf93e5ce8bc1=Dons +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.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.c52888225c69=val +msg.c65e5dfc5ba7=bandiet1 +msg.c72b7844929d=Bandiete verslaan: {arg1}. +msg.c82e3d7279ef=stroom +msg.c86ace9b97f8=Stal gebou. Perde {arg1} van {arg2} +msg.c8da1c4f1714=Word laer in toonhoogte soos die val vorder. +msg.c90399f41b3f=Vleis +msg.c9532ab465fb=been7 +msg.cb8b9677952a=Bergingbelonings: +msg.cb8e70ab2bcc=beerkarkasse +msg.cc167fb645e4=boogstring +msg.cc61d0f3de65=rietmandjies +msg.cccc5c842adb=byl tref +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.d34b6217ea75=vyand val +msg.d373510877fd=Velhandskoene +msg.d39008c4a392=Onbehandelde uitsondering +msg.d49f2e760778=rugsak +msg.d537941485fb=R of I - Derde nota +msg.d609f615ddae=Geen stoorlêers gevind nie. +msg.d6dc139f529a=inval +msg.d6fd3467a950=jeti1 +msg.d75f36f88797=Byle +msg.d7879bd506af=Geen vee is herwin nie. +msg.da0b5a461d98=F of K - Eerste nota (laagste toonhoogte) +msg.da11dc5e090c=slinger tref +msg.da93fb9992a3=vampier sterf +msg.dadd68431e60=Woon in sneeuagtige streke. +msg.db2a8514a0fc=buite bereik gaan +msg.db56e2e1db9f=Avontuur +msg.db5bbabf1bc3=bandiet2 +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.e00d5664a78e=Klei +msg.e0b678c82a60='n Houtbrug span oor 'n diep kloof vorentoe. Die brug het twee stutte +msg.e142524d7689=Strategie: +msg.e1c16d0490ee=Geen geluide beskikbaar nie. +msg.e253d297aabc=veltoonika +msg.e2e91797da96=Visstokke +msg.e30bfd0c38dc=klip +msg.e34161b98188=slegte gooi +msg.e3ce3e543239=Kokers +msg.e3d828480491=slinger hoë reikafstand +msg.e4427f09bc09=Sluit hierdie skerm om te begin. +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.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.e9dd1d268b65=Die basis lê ver na die ooste, bewaak deur 'n barrikade. +msg.ea771032e597=Altaar gebou +msg.ebf229b227f8={arg1} {arg2} van {arg3} +msg.ee200b3d8ae2=Spere +msg.eff5a5f34d9a=eenhoring op brug +msg.f015f25c409b=boog span +msg.f12b2d219fe8=Toue +msg.4671b16b0398=Nie heeltemal 'n zombie of 'n vampier nie. Hierdie ondooie onthou dat hulle voorheen 'n huis gehad het, en dalk is daar nog kos daar... +msg.f305da2c3550==== Vang die Bumerang === +msg.f3c5ed579a54=wind laag +msg.f3d964225e83=speler vroulike skade +msg.f5cabf873590=riet +msg.f77fc018cb7b=Jy vind 'n versteekte bandiete-basis diep in die woud. +msg.f83b6fe3aebf=Verlaat +msg.f85b4b604c9b={arg1}. +msg.f8909d26fe86=ondooie inwoner sterf +msg.f8fc573e2d2f=Laai Spel (geen stoorlêers gevind nie) +msg.fa5bd38090c7=boog +msg.fb4a022a82b3=eenhoring galop +msg.fbec17cb2fcb=reën +msg.fca1af3d07b0=Genesingsrolle +msg.fd1a25749457=Klankvolume {arg1}. +msg.fe2a9d6606e0=draugnorak +msg.feea4a3b0a84==== Eenhoringjag === +msg.fefc084f67de=D of J - Tweede nota +msg.ff5ac0b6df7b=boë diff --git a/lang/en.ini b/lang/en.ini new file mode 100644 index 0000000..478cdc4 --- /dev/null +++ b/lang/en.ini @@ -0,0 +1,1372 @@ +; 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 +main_menu.prompt=Draugnorak. Main menu. +game.new_game_started=New game started. +game.loaded=Game loaded. +game.paused_prompt=Paused. Press backspace to resume. +game.resumed=Resumed. +exit.confirm=Really exit? +direction.west=west +direction.east=east +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. +load_game.save_deleted=Save deleted. +load_game.no_saves_found=No saves found. +load_game.unable_to_load=Unable to load save. +load_game.select_prompt=Load game. Select character. Press delete to remove a 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}. +menu.main.story=The story +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. +inventory.max_arrows_with_quivers=You can only carry {max} arrows with your current quivers. +inventory.item_not_available=Item not available. +search.found_item=Found {item}. +search.found_nothing=Found nothing. +pickup.item=Picked up {item}. +time.period.am=am +time.period.pm=pm +time.report={hour} oclock {period} day {day} +time.report_with_conditions={time}. {conditions} +time.condition.sunny=Sunny +time.condition.daylight=Daylight +time.condition.dark=Dark +time.condition.windy=Windy +time.condition.raining=Raining +time.condition.storming=Storming +time.condition.moon_and_stars=Moon and stars +time.event.area_expanded=The area has expanded! New territory discovered to the east. +time.event.mountain_discovered=A mountain range has been discovered to the east! +time.event.invasion_source_mountains=the mountains +time.event.invasion_source_new_area=the new area +time.event.invasion_start={enemy_plural} are invading from {source}! +time.event.zombie_swarm_spotted=A swarm of zombies has been spotted. +time.event.survivor_joins_one=A survivor joins your base. +time.event.survivor_joins_many={count} survivors join your base. +time.event.base_max_capacity=Your base is at maximum capacity. +time.event.invasion_ended=The {enemy} invasion has ended. +time.event.blessing_speed_fades=The speed blessing fades. +time.event.blessing_resident_fades=The residents' purpose fades. +time.event.blessing_search_fades=The eagle's sight fades. +time.event.sun_setting=The sun is setting. +time.event.sky_brightening=The sky begins to brighten. +time.event.residents_repaired_barricade=Residents repaired the barricade. +{health} health. +time.event.incense_burned_out=The incense has burned out. +combat.burning=Burning! {health} health remaining. +combat.health_status={health} health. +combat.you_died=You have died. +enemies.residents_killed_attacker=Residents killed an attacking {enemy}. +enemies.resident_taken=A resident has been taken. +action.fire.intensity.giant_blaze=Giant blaze +action.fire.intensity.small_fire=Small fire +action.fire.intensity.glowing_coals=A few glowing coals +action.fire.estimate.less_than_one_hour=less than 1 hour +action.fire.estimate.approximately_one_hour=approximately 1 hour +action.fire.estimate.approximately_hours=approximately {hours} hours +action.fire.status={label}. {estimate} remaining. +action.no_fire_nearby=No fire nearby. +action.no_snare_nearby=No snare nearby. +action.snare_holds=Snare holds a {catch_type}. +action.snare_set_inactive=Snare is set but not active yet. +action.snare_set_empty=Snare is set and empty. +action.fish_on_line=Fish on the line. +action.no_fish_on_line=No fish on the line. +action.cannot_place_snares_in_base=Cannot place snares in the base area. +action.snare_already_here=There is already a snare here. +action.snare_set=Snare set. +action.no_snares_to_place=No snares to place. +action.feed_fire.stick=You dump an arm load of sticks into the fire. +action.feed_fire.vine=You toss a few vines and leaves into the fire. +action.feed_fire.log=You heave a log into the fire. +action.no_altar_built=No altar built. +action.need_clay_pot_for_incense=You need a clay pot to burn incense. +action.no_incense_to_burn=No incense to burn. +action.incense_burning=Incense burning. {hours} hours remaining. +action.no_sticks_to_feed_fire=No sticks to feed fire. +action.dumped_sticks=Dumped {amount} sticks into the fire. +action.no_vines_to_feed_fire=No vines to feed fire. +action.dumped_vines=Dumped {amount} vines into the fire. +action.no_logs_to_feed_fire=No logs to feed fire. +action.dumped_logs=Dumped {amount} logs into the fire. +action.burned_incense=Burned {amount} incense. +{hours} hours. +action.option.place_snare=Place Snare +action.option.feed_fire_stick=Feed fire with stick +action.option.feed_fire_vine=Feed fire with vine +action.option.feed_fire_log=Feed fire with log +action.option.burn_incense=Burn incense +action.option.check_fire=Check fire +action.option.check_snare=Check snare +action.option.check_fishing_pole=Check fishing pole +action.menu.prompt=Action menu. {option} +action.cant_do_that=Can't do that. +base_info.not_in_base=You are not in the base. +base_info.menu.no_options=Base info. No options. +base_info.menu.prompt=Base info. {option} +base.barricade_fallen=The barricade has fallen! +base.no_food_residents_hungry=No food, residents are hungry. +base.resident_fishing_pole_broke_one=A resident's fishing pole broke. +base.resident_fishing_pole_broke_many={count} fishing poles broke. +base.resident_caught_fish_one=Resident caught a fish and added it to storage. +base.resident_caught_fish_many=Residents caught {count} fish and added them to storage. +base.resident_smoked_fish=Resident smoked a fish into {yield} smoked fish. +base.livestock_produced=Livestock produced {meat} meat, {skins} skins, and {feathers} feathers and added to storage. +base.resident_spear_broke_one=A resident's spear broke from wear. +base.resident_spear_broke_many={count} spears broke from wear. +base.resident_sling_broke_one=A resident's sling broke from wear. +base.resident_sling_broke_many={count} slings broke from wear. +base.resident_bow_broke_one=A resident's bow broke from wear. +base.resident_bow_broke_many={count} bows broke from wear. +base.resident_clothing_repair_one=A resident is mending clothing. Used {vines} vines, {skins} skins, and {down} down. +base.resident_clothing_repair_many=Residents are mending clothing. Used {vines} vines, {skins} skins, and {down} down. +base.snare_escape_during_check=A {catch_type} escaped while a resident checked the snare at x {position}. +base.resident_retrieved_from_snare=Resident retrieved {game_type} from snare at x {position} y 0 and reset it. +base.resident_knife_broke_butchering=A resident's knife broke while butchering. +base.resident_butchered_result=Resident butchered {game_type}. Added {meat} meat, {skins} skins, {feathers} feathers, {down} down, {sinew} sinew to storage. +base.resident_added_to_storage=Resident added {item} to storage. +base.resident_basket_broke_one=A resident's basket broke. +base.resident_basket_broke_many={count} baskets broke. +base.resident_basket_broke_foraging_one=A resident's basket broke while foraging. +base.resident_basket_broke_foraging_many={count} baskets broke while foraging. +base.resident_gathered_basket_food_one=Resident gathered a basket of fruits and nuts. +base.resident_gathered_basket_food_many=Residents gathered {count} baskets of fruits and nuts. +fishing.size.small=small +fishing.size.medium=medium sized +fishing.size.large=large +fishing.size.monster=monster +fishing.pole_broke.default=Your fishing pole broke. +fishing.pole_broke.switched_weapons=You switched weapons and your fishing pole broke. +fishing.pole_broke.moved_fish_got_away=You moved and the fish got away. Your fishing pole broke. +fishing.pole_broke.moved=You moved and your fishing pole broke. +fishing.require_pole_equipped=You need a fishing pole equipped. +fishing.require_near_stream=You need to be within 2 tiles of a stream. +fishing.fish_on_line=A fish is on the line! +fishing.caught=Caught a {size} {fish}. +fishing.fish_slipped_off_water=The fish slipped off in the water. +fishing.fish_got_away=The fish got away. +fishing.fish_still_on_line=The fish is still on the line. +fishing.stopped=You stop fishing. +altar.must_be_in_base=Must be in base to use altar. +altar.no_altar_built=No altar built. +altar.runed_cannot_sacrifice=Runed items cannot be sacrificed. +altar.nothing_to_sacrifice=Nothing to sacrifice. +altar.sacrificed_one=Sacrificed 1 {item}. Favor +{gain}. Total {total}. +altar.sacrificed_many=Sacrificed {count} {items}. Favor +{gain}. Total {total}. +altar.menu.prompt=Altar. Favor {favor}. +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. +storage.runed_item_name=Runed {equipment} of {rune} +storage.deposited_one=Deposited {name}. +storage.deposited_many=Deposited {amount} {item}. +storage.withdrew_one=Withdrew {name}. +storage.withdrew_many=Withdrew {amount} {item}. +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. +character.info.name_sex=Name {name}, {sex} +character.info.name_unknown=Name unknown +character.info.health=Health {health} of {max} +character.info.weapon=Weapon {weapon} +character.info.clothing_equipped=Clothing equipped: {items} +character.info.no_clothing=No clothing equipped. +character.info.missing=Missing: {slots} +character.info.favor=Favor {favor} +character.info.speed=Speed {status} +character.info.pet=Pet {pet}, health {health} of {max_health}, loyalty {loyalty} of {max_loyalty} +character.info.pet_knocked_out=Knocked out for {hours} {hour_label}. +character.info.pet_none=No pet. +character.word.hour=hour +character.word.hours=hours +pet.default_name=pet +pet.returned=A {pet} returns to you. +pet.knocked_out=Your {pet} has been knocked out. +pet.knocked_out_with_remaining=Your {pet} has been knocked out. {hours} {hour_label} remaining. +pet.leaves={pet} leaves. +pet.leaving=Your {pet} is leaving. +pet.on_the_way=Your pet is on its way. +pet.hungry_unresponsive=A {pet} is hungry and unresponsive. +pet.joins_you=A {pet} joins you. +pet.offer.prompt=A friendly looking {pet} begs for food. Accept? +pet.offer.declined=Declined. +pet.retrieve.cant_carry_chew_on=You can't carry a {item}, so you give it to a {pet} to chew on. +pet.retrieve.retrieved_item=Your {pet} retrieved {item}. +pet.retrieve.random_find=Your {pet} retrieved {count} {item}. +pet.recovered_from_injuries=Your {pet} has recovered from its injuries. +pet.getting_hungry=A {pet} is getting hungry. +fylgja.already_used_today=You have already used your Fylgja today. +fylgja.menu.prompt=Fylgja menu. +fylgja.need_open_ground_to_charge=You need open ground in front of you to charge. +fylgja.option={name} Fylgja +fylgja.connection_with_target=You have a {stage} connection with the {target}. +fylgja.unlocked=You have unlocked the {name} Fylgja! +fylgja.already_unlocked=You have already unlocked the {name} Fylgja. +fylgja.name.unicorn=Unicorn +fylgja.target.unicorn=unicorn +fylgja.stage.tenuous=tenuous +fylgja.stage.faint=faint +fylgja.stage.stirring=stirring +fylgja.stage.budding=budding +fylgja.stage.kindled=kindled +fylgja.stage.bound=bound +fylgja.stage.sworn=sworn +fylgja.stage.ascendant=ascendant +fylgja.stage.ultimate=ultimate +quest.name.bat_invasion=Bat Invasion +quest.name.catch_boomerang=Catch the Boomerang +quest.name.enchanted_melody=Enchanted Melody +quest.name.escape_from_hel=Escape from Hel +quest.name.skeletal_bard=Skeletal Bard +quest.name.unknown=Unknown Quest +quest.description.bat_invasion=Bat Invasion. Giant killer bats are attacking. Press space to throw when the bat is centered. +quest.description.catch_boomerang=Catch the Boomerang. Throw and catch the boomerang as it returns for up to 4 points. +quest.description.enchanted_melody=Enchanted Melody. Repeat the pattern using E R D F or U I J K. Lowest to highest pitch. +quest.description.escape_from_hel=Escape from Hel. Press space to jump over open graves. The pace quickens. +quest.description.skeletal_bard=Skeletal Bard. A skeleton named Billy Bones is practicing to become a bard. Count the notes. +quest.description.unknown=Unknown quest. +quest.new_available=A new quest is available: {quest}. +quest.none_available=No quests available. +quest.favor_phrase.displeased=The gods are displeased with your performance. +quest.favor_phrase.pleased=The gods are pleased with your performance. +quest.favor_phrase.very_pleased=The gods are very pleased with your performance. +quest.favor_phrase.extremely_pleased=The gods are extremely pleased with your performance. +quest.favor_phrase.ultimately_pleased=The gods are ultimately pleased with your performance. +quest.rewards.title=Quest Rewards +quest.rewards.complete_heading=Quest Complete! +quest.rewards.heading=Rewards: +quest.rewards.favor_line=Favor: +{favor} +quest.rewards.item_line={item}: +{amount} +quest.rewards.inventory_full_line=Inventory full, could not receive {item}. +quest.rewards.no_items_awarded=No items awarded. +quest.rewards.score_line=Score: {score} +quest.menu.prompt=Quest menu. +quest.menu.count={index} of {total} +quest.minigame.starting=Starting. +quest.bat_invasion.complete_score=Bat invasion complete. Score {score}. +quest.boomerang.press_space_to_throw=Press Space to throw. +quest.boomerang.skill.almost_dropped=almost dropped +quest.boomerang.skill.managed_to_catch=managed to catch +quest.boomerang.skill.caught=caught +quest.boomerang.skill.expertly_caught=expertly caught +quest.boomerang.caught_result=You {skill} the boomerang. {points} points.{suffix} +quest.boomerang.missed=You missed the boomerang.{suffix} +quest.boomerang.hit_you=The boomerang hit you.{suffix} +quest.boomerang.summary=You caught {count} boomerangs for a total of {score} points. +quest.enchanted_melody.practice_prompt=Practice mode. Press Enter to begin, Escape to cancel. +quest.enchanted_melody.starting_prompt=Starting. Repeat the pattern. +quest.enchanted_melody.score=You matched {rounds} notes. Score {score}. +quest.skeletal_bard.select_notes=Select number of notes. {selection}. +quest.skeletal_bard.round_result=You entered {guess} notes and the actual number was {actual}. {points} points. Press Enter to continue. +quest.skeletal_bard.complete_score=Skeletal Bard complete. Your score for all five tunes is {score}. +quest.escape_from_hel.fell_in_score=You fell in. Score {score}. +adventure.none_in_base=No adventures available in the base. +adventure.already_attempted_today=You have already attempted an adventure today. +adventure.option.unicorn_hunt=Unicorn Hunt (Mountain Boss) +adventure.option.bandit_hideout=Bandit's Hideout +adventure.none_in_area=No adventures found in this area. +adventure.menu.prompt=Adventure menu. +adventure.victory=Victory! +adventure.rewards.title==== Victory Rewards === +adventure.unicorn.flee=You fled from the unicorn. +adventure.unicorn.position_report=X {x}, {terrain}, facing {direction} +adventure.unicorn.player_trampled=The unicorn trampled you! +adventure.unicorn.victory_title=Unicorn Victory +adventure.unicorn.reward.favor_awarded=The gods are pleased with your victory! {favor} favor awarded. +adventure.unicorn.reward.heal_scrolls=Heal Scrolls: +{count}. +adventure.unicorn.reward.learned_rune_swiftness=Learned Rune of Swiftness! +adventure.unicorn.reward.engrave_unlocked=You can now engrave equipment with this rune at the crafting menu. +adventure.unicorn.reward.mastered_rune_swiftness=You have already mastered the Rune of Swiftness. +adventure.unicorn.horses.no_stable_or_storage=Stable or storage not built. No horses were captured. +adventure.unicorn.horses.stable_full=Stable is full. No horses were captured. +adventure.unicorn.horses.joined_stable=A horse joins your stable. +adventure.unicorn.horses.none_captured=No horses were captured. +adventure.bandit_hideout.flee=You fled from the bandit hideout. +adventure.bandit_hideout.position_report=X {x}, {terrain}. Distance to base {distance}. Facing {direction}. Base health {health} of {max}. +adventure.bandit_hideout.player_defeated=You were defeated in the bandit hideout. +character.report.position={direction}, x {x}, y {y}, terrain {terrain} +character.report.terrain_mountains={terrain}. Mountains +character.report.player={health} of {max} health +character.report.pet={pet}: {health} of {max} health, loyalty {loyalty} of {max_loyalty} +item.label.items=items +item.label.item=item +item.label.unknown=Unknown +equipment.menu.prompt=Equipment menu. {option} +equipment.menu.nothing_to_equip=Nothing to equip. +equipment.menu.equipped_suffix= (equipped) +equipment.set_to_slot={name} set to slot {slot}. +equipment.equipped={name} equipped. +equipment.unequipped={name} unequipped. +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. +environment.fall.damage_report=Fell {height} feet! Took {damage} damage. {health} health remaining. +environment.snare.collected_with_catch=Collected {catch_type} and snare. +environment.snare.collected_empty=Collected snare. +environment.tree.cut_down=This tree has been cut down. +environment.tree.empty=This tree is empty. +environment.area.nothing_left=This area has nothing left. +environment.tree.climb_started=Started climbing tree. Height is {height} feet. +environment.tree.climb_reached_top=Reached the top at {height} feet. +environment.tree.climb_reached_ground=Safely reached the ground. +environment.tree.climb_down=Climbing down. +environment.movement.need_rope=You'll need a rope to climb there. +environment.movement.press_up_climb=Press up to climb up. +environment.movement.press_down_climb=Press down to climb down. +environment.movement.need_canoe=You need a canoe to cross deep water. +environment.rope_climb.direction.up=up +environment.rope_climb.direction.down=down +environment.rope_climb.progress=Climbing {direction}. {distance} feet. +environment.rope_climb.reached_elevation=Reached elevation {elevation}. +snare.escape_at=A {catch_type} escaped from your snare at x {position} y 0! +snare.caught_at={catch_type} caught in snare at x {position} y 0! +snare.stepped_on_with_escape=You stepped on your snare! The {catch_type} escaped. +snare.stepped_on_broke=You stepped on your snare and broke it! +fire.low_fuel_at=Fire at x {position} y {y} is getting low! +fire.went_out_at=Fire at x {position} y {y} has gone out. +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.in_progress=Crafting... +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.weapons.crafted.spear=Crafted a Spear. +crafting.weapons.crafted.sling=Crafted a Sling. +crafting.weapons.crafted.bow=Crafted a Bow. +crafting.weapons.crafted.stone_axe=Crafted a Stone Axe. +crafting.weapons.crafted_max.spears=Crafted {count} Spears. +crafting.weapons.crafted_max.slings=Crafted {count} Slings. +crafting.weapons.crafted_max.bows=Crafted {count} Bows. +crafting.weapons.crafted_max.stone_axes=Crafted {count} Stone Axes. +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.tools.crafted.stone_knife=Crafted a Stone Knife. +crafting.tools.crafted.snare=Crafted a Snare. +crafting.tools.crafted.fishing_pole=Crafted a Fishing Pole. +crafting.tools.crafted.rope=Crafted rope. +crafting.tools.crafted.quiver=Crafted a Quiver. +crafting.tools.crafted.canoe=Crafted a Canoe. +crafting.tools.crafted.reed_basket=Crafted a reed basket. +crafting.tools.crafted.clay_pot=Crafted a clay pot. +crafting.tools.crafted_max.stone_knives=Crafted {count} Stone Knives. +crafting.tools.crafted_max.snares=Crafted {count} Snares. +crafting.tools.crafted_max.fishing_poles=Crafted {count} Fishing Poles. +crafting.tools.crafted_max.ropes=Crafted {count} Rope. +crafting.tools.crafted_max.quivers=Crafted {count} Quivers. +crafting.tools.crafted_max.canoes=Crafted {count} Canoes. +crafting.tools.crafted_max.reed_baskets=Crafted {count} Reed Baskets. +crafting.tools.crafted_max.clay_pots=Crafted {count} Clay Pots. +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.materials.not_enough_quiver_capacity_for_arrows=Not enough quiver capacity for arrows. +crafting.materials.crafted.arrows=Crafted {count} arrows. +crafting.materials.crafted.bowstring=Crafted a bowstring. +crafting.materials.crafted_max.bowstrings=Crafted {count} Bowstrings. +crafting.materials.crafted.incense=Crafted incense. +crafting.materials.crafted_max.incense=Crafted {count} Incense. +crafting.materials.smoke_fish.crafted=Smoked a fish into {yield} smoked fish. +crafting.materials.smoke_fish.crafted_max=Smoked {count} fish into {yield} smoked fish. +crafting.materials.butcher.goose=Butchered goose. Got 1 meat, feathers, and down. +crafting.materials.butcher.turkey=Butchered turkey. Got 1 meat and feathers. +crafting.materials.butcher.boar=Butchered boar. Got meat, 3 skins, and 2 sinew. +crafting.materials.butcher.default=Butchered {game}. Got 1 meat and 1 skin. +crafting.materials.butcher.no_space_outputs=No space for outputs. +crafting.materials.butcher.max_result=Butchered {count} game. Got {meat} meat, {skins} skins, {feathers} feathers, {down} down, {sinew} sinew. +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.clothing.crafted.skin_hat=Crafted a Skin Hat. +crafting.clothing.crafted.skin_gloves=Crafted Skin Gloves. +crafting.clothing.crafted.skin_pants=Crafted Skin Pants. +crafting.clothing.crafted.skin_tunic=Crafted a Skin Tunic. +crafting.clothing.crafted.moccasins=Crafted moccasins. +crafting.clothing.crafted.skin_pouch=Crafted a Skin Pouch. +crafting.clothing.crafted.backpack=Crafted a Backpack. +crafting.clothing.crafted_max.skin_hats=Crafted {count} Skin Hats. +crafting.clothing.crafted_max.skin_gloves=Crafted {count} Skin Gloves. +crafting.clothing.crafted_max.skin_pants=Crafted {count} Skin Pants. +crafting.clothing.crafted_max.skin_tunics=Crafted {count} Skin Tunics. +crafting.clothing.crafted_max.moccasins=Crafted {count} Moccasins. +crafting.clothing.crafted_max.skin_pouches=Crafted {count} Skin Pouches. +crafting.clothing.crafted_max.backpacks=Crafted {count} Backpacks. +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.buildings.none_available=No buildings available. +crafting.buildings.firepit.already_in_base=There is already a firepit in the base. +crafting.buildings.firepit.already_here=There is already a firepit here. +crafting.buildings.firepit.built=Firepit built here. +crafting.buildings.fire.requires_firepit_nearby=You need a firepit within 2 tiles to build a fire. +crafting.buildings.fire.already_in_base=There is already a fire in the base. +crafting.buildings.fire.built=Fire built at firepit. +crafting.buildings.herb_garden.base_only=Herb garden can only be built in the base area. +crafting.buildings.herb_garden.already_in_base=There is already an herb garden in the base. +crafting.buildings.herb_garden.built=Herb garden built. The base now heals faster. +crafting.buildings.storage.base_only=Storage must be built in the base. +crafting.buildings.storage.fully_upgraded=Storage is fully upgraded. +crafting.buildings.storage.upgraded=Storage upgraded. Capacity is now {capacity} per item. +crafting.buildings.pasture.base_only=Pasture must be built in the base. +crafting.buildings.pasture.requires_storage_upgrade=Storage must be upgraded before a pasture. +crafting.buildings.pasture.already_built=Pasture already built. +crafting.buildings.pasture.built=Pasture built. +crafting.buildings.stable.base_only=Stable must be built in the base. +crafting.buildings.stable.requires_storage_upgrade=Storage must be upgraded before a stable. +crafting.buildings.stable.already_built=Stable already built. +crafting.buildings.stable.built=Stable built. +crafting.buildings.altar.base_only=Altar must be built in the base. +crafting.buildings.altar.already_built=Altar already built. +crafting.buildings.altar.built=Altar built. +crafting.runes.base_only=Rune engraving can only be done in the base area. +crafting.runes.option={rune} (1 Clay, 1 Favor) [Requires Knife] +crafting.runes.none_unlocked=No runes unlocked yet. +crafting.runes.prompt=Runes. {option} +crafting.runes.equipment_option={name} ({count} available) +crafting.runes.no_equipment_available=No equipment available to engrave. +crafting.runes.select_equipment_prompt=Select equipment to engrave with {rune}. {option} +crafting.runes.no_item_available=No {item} available. +crafting.runes.runed_name=Runed {equipment} of {rune} +crafting.runes.engraved=Engraved {item}. +crafting.runes.engraved_max=Engraved {count} {item} with {rune}. +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.barricade.full_health=Barricade is already at full health. +crafting.barricade.no_materials=No materials to reinforce the barricade. +crafting.barricade.reinforced.sticks=Reinforced barricade with sticks. +{gained} health. Now {health} of {max}. +crafting.barricade.reinforced.vines=Reinforced barricade with vines. +{gained} health. Now {health} of {max}. +crafting.barricade.reinforced.log=Reinforced barricade with log. +{gained} health. Now {health} of {max}. +crafting.barricade.reinforced.stones=Reinforced barricade with stones. +{gained} health. Now {health} of {max}. +crafting.barricade.reinforced_max.sticks=Reinforced barricade {count} times with sticks. Health now {health}. +crafting.barricade.reinforced_max.vines=Reinforced barricade {count} times with vines. Health now {health}. +crafting.barricade.reinforced_max.log=Reinforced barricade {count} times with log. Health now {health}. +crafting.barricade.reinforced_max.stones=Reinforced barricade {count} times with stones. Health now {health}. +crafting.barricade.not_enough.sticks=Not enough sticks. +crafting.barricade.not_enough.vines=Not enough vines. +crafting.barricade.not_enough.logs=Not enough logs. +crafting.barricade.not_enough.stones=Not enough stones. +crafting.missing=Missing: {requirements} +crafting.requirement.stone_knife=Stone Knife +crafting.requirement.game=Game +crafting.requirement.favor=favor +crafting.requirement.resources=resources +story.voice.format={speaker}: {line} +story.god.fallback=A god +story.god.odin=Odin +story.god.thor=Thor +story.god.freyja=Freyja +story.god.loki=Loki +story.god.tyr=Tyr +story.god.baldur=Baldur +story.god.frigg=Frigg +story.god.heimdall=Heimdall +story.god.hel=Hel +story.god.fenrir=Fenrir +story.god.freyr=Freyr +story.line01=The world has fallen. +story.line02=Once-great cities lie in ruin. +story.line03=In most places, not two stones remain as mankind set them. +story.line04=The gods, long thought sleeping, achieved the impossible. +story.line05=Ragnarok was averted. +story.line06=Great Fenrir was freed and did not strike down his captors. +story.line07=Surtr did not lay waste to our world with his flaming greatsword. +story.line08=For a fleeting age, the gods wrestled fate into silence. +story.line09=But the end of the world was delayed, never denied. +story.line10=One force lay beyond even divine command: mankind. +story.line11=Hatred swelled. +story.line12=Greed answered it. +story.line13=Mercy withered. +story.line14=Differences became crimes. +story.line15=Folk no longer argued; they tore at one another like beasts. +story.line16=Neighbors became enemies for trifles: a color, a song, a single word. +story.line17=War followed. +story.line18=Cities burned, roads broke, and monsters wearing human faces roamed the streets. +story.line19=Then ruin deepened into terror. +story.line20=The dead, enraged by wanton slaughter, rose from their graves. +story.line21=Corpses woke and hunted the living. +story.line22=Night-stalkers fed on blood. +story.line23=Even ash and dust remembered form and rose again. +story.line24=Soon all that remained was rubble and bone. +story.line25=Your last memory is flight from a vast horde of nightmare creatures. +story.line26=When you wake, you are somewhere strange. +story.line27=A crude wooden shelter stands against the wind, open to the elements, braced with sticks and logs. +story.line28=As your senses return, faint voices stir in the dark. +story.line29=After the death of this world, another shall rise. +story.line30=You have been judged worthy to try and survive. +story.line31=This world belongs to the dead. +story.line32=In time, they will slay even us. +story.line33=Prove yourself, warrior, and you may walk into the next world. +story.line34=May your death, when it comes, be honorable. +story.line35=We will aid you while we still have strength, but time is against us. +story.line36=Beyond your shelter lies grassland, and farther off, broken stone that may once have been a city. +story.line37=In the total devastation, even memory has lost its shape. + +[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/item_registry.nvgt:item_plural:backpacks +msg.03268c7d7239=backpacks +; src/item_registry.nvgt:item_singular:rope +msg.03976920927e=rope +; src/item_registry.nvgt:item_display:small_game +msg.0406529811c7=Small Game +; draugnorak.nvgt:608: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/bosses/bandit_hideout.nvgt:787:insert_last[0] +msg.066723a92e94==== Victory Rewards === +; 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/sound_settings.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/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/bosses/bandit_hideout.nvgt:305:insert_last[0] +msg.0b3d8d5cdca6=- Reach the base and destroy the barricade +; seed:learn_sounds_label:sounds/terrain/forest.ogg +msg.0bcd9af79f2d=forest +; seed:learn_sounds_label:sounds/crafting_complete.ogg +msg.0c7789cf9a52=crafting complete +; src/item_registry.nvgt:item_display:arrows +msg.0cf604cb001b=Arrows +; seed:learn_sounds_label:sounds/weapons/sling_swing.ogg +msg.0d1ec2e04286=sling swing +; src/item_registry.nvgt:item_plural:knives +msg.0e16a11cda9a=knives +; src/item_registry.nvgt:item_singular:spear +msg.0e57fb642209=spear +; src/item_registry.nvgt:item_display:slings +msg.0eb796e65c39=Slings +; src/item_registry.nvgt:item_singular:skin_pouch +msg.0f9af12cbb82=skin pouch +; src/item_registry.nvgt:item_display:logs +msg.126dd3b70a5c=Logs +; libstorm-nvgt/file_viewer.nvgt:74:screen_reader_speak[0] +msg.136e4cb03bd5=Failed to save file +; src/item_registry.nvgt:item_plural:quivers +msg.145ba8a7a0f2=quivers +; src/bosses/bandit_hideout.nvgt:860:insert_last[0] +msg.14f5f5ff4f76=No survivors were found. +; seed:learn_sounds_label:sounds/game/game_falls.ogg +msg.16134a4250f7=game falls +; src/quests/escape_from_hel_game.nvgt:12:insert_last[0] +msg.16a233f44489=- Listen for open graves approaching (growing louder) +; seed:manual +msg.176b9a0c8398=Really exit? +; src/item_registry.nvgt:item_singular:incense_stick +msg.17808e3775e1=incense stick +; src/quests/skeletal_bard_game.nvgt:44:insert_last[0] +msg.17b5b2a286cc==== Skeletal Bard === +; seed:learn_sounds_label:sounds/quests/bat1.ogg +msg.18b837bb6f43=bat1 +; seed:learn_sounds_label:sounds/enemies/yeti_dies.ogg +msg.18c77e824a2a=yeti dies +; seed:learn_sounds_label:sounds/enemies/bandit_dies.ogg +msg.1a853cbfbc36=bandit dies +; src/sound_settings.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/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/bosses/bandit_hideout.nvgt:806: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/item_registry.nvgt:item_singular:axe +msg.237b5017397b=axe +; src/bosses/unicorn/unicorn_boss.nvgt:117:insert_last[0] +msg.245c89590526=- When the bridge collapses with the Unicorn on it, you win! +; src/bosses/bandit_hideout.nvgt:834: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 +; 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/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:52: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:817: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/quests/bat_invasion_game.nvgt:19:text_reader_lines[1] +; src/quests/catch_the_boomerang_game.nvgt:26:text_reader_lines[1] +; src/quests/enchanted_melody_game.nvgt:39:text_reader_lines[1] +msg.29f291640516=Quest Instructions +; src/quests/escape_from_hel_game.nvgt:11:insert_last[0] +msg.2afb87ae1752=- You run automatically, speed increases over time +; src/sound_settings.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 +; seed:learn_sounds_label:sounds/weapons/spear_swing.ogg +msg.2e094dd0a2ab=spear swing +; src/item_registry.nvgt:item_display:fish +msg.3030e8ec7633=Fish +; src/quests/skeletal_bard_game.nvgt:50:insert_last[0] +msg.33bc40d8201e=- After the tune, choose the number of notes +; 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/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/item_registry.nvgt:item_plural:skins +msg.37423f3bd00f=skins +; 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/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/item_registry.nvgt:item_plural:skin_pouches +msg.3c32451c9c71=skin pouches +; seed:learn_sounds_label:sounds/actions/fishpole.ogg +msg.3cebb4c50866=fishpole +; seed:learn_sounds_label:sounds/terrain/hard_stone.ogg +msg.3e024b87e650=hard stone +; src/item_registry.nvgt:item_display:skins +msg.3e03229d39e4=Skins +; src/item_registry.nvgt:item_plural:skin_gloves +msg.3eb9cd52ec8d=skin gloves +; src/quests/enchanted_melody_game.nvgt:34:insert_last[0] +msg.3f638a51cb47=E or U - Fourth note (highest pitch) +; 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/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/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} +; seed:learn_sounds_label:sounds/quests/spear_miss.ogg +msg.437ecef821db=spear miss +; src/item_registry.nvgt:item_plural:bowstrings +msg.43821cae64c5=bowstrings +; seed:learn_sounds_label:sounds/actions/falling.ogg +msg.444d4678962b=falling +; seed:learn_sounds_label:sounds/weapons/bow_fire.ogg +msg.4537c16badd9=bow fire +; src/item_registry.nvgt:item_plural:snares +msg.455b11a16eb7=snares +; src/sound_settings.nvgt:38:learn_sounds_add_description[1] +msg.4671b16b0398=Not quite a zombie nor a vampyr. These undead remember they used to have a home, and maybe there's food there... +; seed:learn_sounds_label:sounds/terrain/draugrhaugr_step.ogg +msg.46fcdd91c99e=draugrhaugr step +; src/item_registry.nvgt:item_plural:logs +msg.474c797713f3=logs +; src/item_registry.nvgt:item_display:snares +msg.483a17dd2a17=Snares +; 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/item_registry.nvgt:item_plural:stones +msg.4c0d2469f78b=stones +; seed:learn_sounds_label:sounds/notify.ogg +msg.4f08301dcb7b=notify +; seed:learn_sounds_label:sounds/terrain/shallow_water.ogg +msg.4f11c6797b8d=shallow water +; 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 +; seed:learn_sounds_label:sounds/game/turkey.ogg +msg.5009c8e190ee=turkey +; src/quests/skeletal_bard_game.nvgt:51: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 +; draugnorak.nvgt:81:insert_last[0] +msg.528fb1c4e5fb=New Game +; seed:learn_sounds_label:sounds/quests/squish.ogg +msg.5460f2e3c43e=squish +; seed:learn_sounds_label:sounds/actions/fishpole_break.ogg +msg.546330ee64db=fishpole break +; draugnorak.nvgt:608:ui_info_box[0] +msg.54fc2d657ed6=Draugnorak +; seed:learn_sounds_label:sounds/enemies/vampyr3.ogg +msg.55748cf43121=vampyr3 +; src/quests/bat_invasion_game.nvgt:9:insert_last[0] +msg.5665ef30ef4d=Bats are invading! Throw your spear to defend. +; 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. +; seed:learn_sounds_label:sounds/enemies/wight_dies.ogg +msg.5b61e4982b59=wight dies +; src/item_registry.nvgt:item_display:feathers +msg.5bdb7fa2223d=Feathers +; src/item_registry.nvgt:item_plural:reeds +msg.5c2434ecd90f=reeds +; src/bosses/bandit_hideout.nvgt:829: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. +; seed:learn_sounds_label:sounds/actions/cast_strength.ogg +msg.5dfc869b7588=cast strength +; src/time_system.nvgt:793: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/menus/base_info.nvgt:12:insert_last[0] +msg.623f78278373=Barricade health {arg1} of {arg2} +; src/bosses/bandit_hideout.nvgt:827: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 +; 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/item_registry.nvgt:item_plural:skin_pants +; src/item_registry.nvgt:item_singular:skin_pants +msg.65eb0fb6ab40=skin pants +; 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:21:insert_last[0] +msg.6861fd95f4f7=- There are 5 turns +; 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 +; seed:learn_sounds_label:sounds/actions/hit_ground.ogg +msg.704e634dd69f=hit ground +; seed:learn_sounds_label:sounds/actions/set_snare.ogg +msg.714165bec103=set snare +; src/quests/skeletal_bard_game.nvgt:53: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] +msg.731152a028c6=Closed. +; src/quests/catch_the_boomerang_game.nvgt:20:insert_last[0] +msg.7680f62917b1=- Press Space again when it sounds close +; 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/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/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/quests/skeletal_bard_game.nvgt:47: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/sound_settings.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:194:ui_info_box[1] +; draugnorak.nvgt:208: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/time_system.nvgt:775:notify[0] +; src/time_system.nvgt:785:notify[0] +msg.81df04a81e24={arg1} favor shines upon you. {arg2} +; src/quests/catch_the_boomerang_game.nvgt:22:insert_last[0] +msg.823be948275f=- Better timing earns more points (up to 4) +; src/sound_settings.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. +; 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! +; 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 +; seed:learn_sounds_label:sounds/quests/bone5.ogg +msg.86290fe7e711=bone5 +; seed:learn_sounds_label:sounds/dialog.ogg +msg.863be329d064=dialog +; 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/time_system.nvgt:789:notify[0] +msg.8818f1d55166={arg1} radiance fills residents with purpose. +; 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/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:16: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! +; seed:learn_sounds_label:sounds/quests/bee.ogg +msg.917ee46db0cd=bee +; seed:learn_sounds_label:sounds/player_male_damage.ogg +msg.924e0eb6b6f1=player male damage +; src/time_system.nvgt:780:notify[0] +msg.9281d2fe5f08={arg1} favor shines upon you. You feel swift for a while. +; seed:learn_sounds_label:sounds/actions/climb_rope.ogg +msg.92abc6d6953f=climb rope +; src/bosses/bandit_hideout.nvgt:800:insert_last[0] +msg.9379874d592b=Altar not built. No favor awarded. +; src/bosses/unicorn/unicorn_boss.nvgt:113:insert_last[0] +msg.93894d088f6a=- Use your axe to destroy a bridge support +; src/item_registry.nvgt:item_singular:stick +; seed:learn_sounds_label:sounds/items/stick.ogg +msg.946466f6c963=stick +; seed:learn_sounds_label:sounds/game/boar.ogg +msg.957f554c342a=boar +; src/bosses/bandit_hideout.nvgt:857:insert_last[0] +msg.97c59037a63c=A survivor joins your base. +; src/item_registry.nvgt:item_plural:smoked_fish +; src/item_registry.nvgt:item_singular:smoked_fish +msg.995d4e06ae4e=smoked fish +; src/sound_settings.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:19:insert_last[0] +msg.9afdb7e61391=- Press Space to throw +; src/bosses/bandit_hideout.nvgt:865:text_reader_lines[1] +msg.9b64062b2074=Bandit's Hideout +; 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/quests/bat_invasion_game.nvgt:11:insert_last[0] +; src/quests/catch_the_boomerang_game.nvgt:18:insert_last[0] +; src/quests/escape_from_hel_game.nvgt:10:insert_last[0] +msg.a040b931b9bb=How to play: +; 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/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 +; src/sound_settings.nvgt:43:learn_sounds_add_description[1] +; src/sound_settings.nvgt:44:learn_sounds_add_description[1] +; src/sound_settings.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:798:insert_last[0] +msg.a646f0dcb735=Favor awarded: {arg1}. +; 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/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 +; 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 +; seed:manual +msg.abe73973cc7c=Unable to load save. +; src/quests/enchanted_melody_game.nvgt:30:insert_last[0] +msg.ac36af678d78=Controls: +; src/item_registry.nvgt:item_singular:moccasin +msg.ad34f00c46cd=moccasin +; seed:learn_sounds_label:sounds/enemies/goblin_dies.ogg +msg.ad36d64ad3a3=goblin dies +; 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/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 +; 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/item_registry.nvgt:item_display:skin_tunics +msg.b48d0661f0da=Skin Tunics +; seed:learn_sounds_label:sounds/enemies/bandit3.ogg +msg.b5d5cc084a95=bandit3 +; seed:learn_sounds_label:sounds/actions/call_pet.ogg +msg.b73a0709297f=call pet +; src/item_registry.nvgt:item_display:knives +msg.b774102c89b5=Knives +; seed:learn_sounds_label:sounds/terrain/snow.ogg +msg.b94e9f3d7e00=snow +; src/bosses/bandit_hideout.nvgt:844: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/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 +; seed:learn_sounds_label:sounds/weapons/arrow_hit.ogg +msg.bdebda25dad9=arrow hit +; src/item_registry.nvgt:item_display:down +msg.bf93e5ce8bc1=Down +; src/sound_settings.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:821:insert_last[0] +msg.c05feaca7269=No items were recovered. +; src/item_registry.nvgt:item_singular:snare +msg.c1104d82c0bd=snare +; src/sound_settings.nvgt:37:learn_sounds_add_description[1] +msg.c48d642737b2=Inhabits mountainous regions. +; src/item_registry.nvgt:item_display:smoked_fish +msg.c4f7e9c21601=Smoked Fish +; src/sound_settings.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. +; seed:learn_sounds_label:sounds/quests/fall.ogg +msg.c52888225c69=fall +; seed:learn_sounds_label:sounds/enemies/bandit1.ogg +msg.c65e5dfc5ba7=bandit1 +; src/bosses/bandit_hideout.nvgt:789: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/sound_settings.nvgt:33:learn_sounds_add_description[1] +msg.c8da1c4f1714=Lowers in pitch as the fall progresses. +; src/item_registry.nvgt:item_display:meat +msg.c90399f41b3f=Meat +; seed:learn_sounds_label:sounds/quests/bone7.ogg +msg.c9532ab465fb=bone7 +; src/bosses/bandit_hideout.nvgt:808: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/item_registry.nvgt:item_plural:reed_baskets +msg.cc61d0f3de65=reed baskets +; seed:learn_sounds_label:sounds/weapons/axe_hit.ogg +msg.cccc5c842adb=axe hit +; 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:316:insert_last[0] +msg.ce3897c6b3be={arg1}: {arg2} +; seed:learn_sounds_label:sounds/items/item_breaks.ogg +msg.ce5aeca95fb9=item breaks +; src/i18n.nvgt:677:speak_with_history[0] +; src/pet_system.nvgt:926: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 +; 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:608: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 +; seed:manual +msg.d609f615ddae=No saves found. +; 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:836:insert_last[0] +msg.d7879bd506af=No livestock were recovered. +; 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/sound_settings.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/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 +; 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:46: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/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/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/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 +; seed:learn_sounds_label:sounds/actions/bad_cast.ogg +msg.e34161b98188=bad cast +; 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:24:insert_last[0] +; src/quests/escape_from_hel_game.nvgt:17:insert_last[0] +msg.e4427f09bc09=Close this screen to begin. +; 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:847: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 +; 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:96:insert_last[0] +msg.e8db3a42f86f=Learn Sounds +; src/bosses/bandit_hideout.nvgt:845:insert_last[0] +msg.e8e8f4ee7a5a=You can now engrave weapons with this rune at the crafting menu. +; src/bosses/bandit_hideout.nvgt:302:insert_last[0] +msg.e9dd1d268b65=The base lies far to the east, guarded by a barricade. +; src/menus/base_info.nvgt:36:insert_last[0] +msg.ea771032e597=Altar built +; libstorm-nvgt/notifications.nvgt:153:notifications_speak[0] +; libstorm-nvgt/notifications.nvgt:173:notifications_speak[0] +msg.ebf229b227f8={arg1} {arg2} of {arg3} +; 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/item_registry.nvgt:item_display:ropes +msg.f12b2d219fe8=Ropes +; src/quests/catch_the_boomerang_game.nvgt:14:insert_last[0] +msg.f305da2c3550==== Catch the Boomerang === +; 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/item_registry.nvgt:item_singular:reed +msg.f5cabf873590=reed +; src/bosses/bandit_hideout.nvgt:301:insert_last[0] +msg.f77fc018cb7b=You find a hidden bandit base deep in the forest. +; draugnorak.nvgt:112:insert_last[0] +msg.f83b6fe3aebf=Exit +; src/inventory_items.nvgt:460:speak_with_history[0] +; src/quests/skeletal_bard_game.nvgt:26:speak_with_history[0] +; src/quests/skeletal_bard_game.nvgt:32:speak_with_history[0] +msg.f85b4b604c9b={arg1}. +; seed:learn_sounds_label:sounds/enemies/undead_resident_dies.ogg +msg.f8909d26fe86=undead resident dies +; seed:manual +msg.f8fc573e2d2f=Load Game (no saves found) +; src/item_registry.nvgt:item_singular:bow +msg.fa5bd38090c7=bow +; 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/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}. +; seed:learn_sounds_label:sounds/menu/Draugnorak.ogg +; seed:learn_sounds_label:sounds/menu.bak/Draugnorak.ogg +msg.fe2a9d6606e0=draugnorak +; 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 diff --git a/lang/en.template.ini b/lang/en.template.ini new file mode 100644 index 0000000..478cdc4 --- /dev/null +++ b/lang/en.template.ini @@ -0,0 +1,1372 @@ +; 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 +main_menu.prompt=Draugnorak. Main menu. +game.new_game_started=New game started. +game.loaded=Game loaded. +game.paused_prompt=Paused. Press backspace to resume. +game.resumed=Resumed. +exit.confirm=Really exit? +direction.west=west +direction.east=east +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. +load_game.save_deleted=Save deleted. +load_game.no_saves_found=No saves found. +load_game.unable_to_load=Unable to load save. +load_game.select_prompt=Load game. Select character. Press delete to remove a 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}. +menu.main.story=The story +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. +inventory.max_arrows_with_quivers=You can only carry {max} arrows with your current quivers. +inventory.item_not_available=Item not available. +search.found_item=Found {item}. +search.found_nothing=Found nothing. +pickup.item=Picked up {item}. +time.period.am=am +time.period.pm=pm +time.report={hour} oclock {period} day {day} +time.report_with_conditions={time}. {conditions} +time.condition.sunny=Sunny +time.condition.daylight=Daylight +time.condition.dark=Dark +time.condition.windy=Windy +time.condition.raining=Raining +time.condition.storming=Storming +time.condition.moon_and_stars=Moon and stars +time.event.area_expanded=The area has expanded! New territory discovered to the east. +time.event.mountain_discovered=A mountain range has been discovered to the east! +time.event.invasion_source_mountains=the mountains +time.event.invasion_source_new_area=the new area +time.event.invasion_start={enemy_plural} are invading from {source}! +time.event.zombie_swarm_spotted=A swarm of zombies has been spotted. +time.event.survivor_joins_one=A survivor joins your base. +time.event.survivor_joins_many={count} survivors join your base. +time.event.base_max_capacity=Your base is at maximum capacity. +time.event.invasion_ended=The {enemy} invasion has ended. +time.event.blessing_speed_fades=The speed blessing fades. +time.event.blessing_resident_fades=The residents' purpose fades. +time.event.blessing_search_fades=The eagle's sight fades. +time.event.sun_setting=The sun is setting. +time.event.sky_brightening=The sky begins to brighten. +time.event.residents_repaired_barricade=Residents repaired the barricade. +{health} health. +time.event.incense_burned_out=The incense has burned out. +combat.burning=Burning! {health} health remaining. +combat.health_status={health} health. +combat.you_died=You have died. +enemies.residents_killed_attacker=Residents killed an attacking {enemy}. +enemies.resident_taken=A resident has been taken. +action.fire.intensity.giant_blaze=Giant blaze +action.fire.intensity.small_fire=Small fire +action.fire.intensity.glowing_coals=A few glowing coals +action.fire.estimate.less_than_one_hour=less than 1 hour +action.fire.estimate.approximately_one_hour=approximately 1 hour +action.fire.estimate.approximately_hours=approximately {hours} hours +action.fire.status={label}. {estimate} remaining. +action.no_fire_nearby=No fire nearby. +action.no_snare_nearby=No snare nearby. +action.snare_holds=Snare holds a {catch_type}. +action.snare_set_inactive=Snare is set but not active yet. +action.snare_set_empty=Snare is set and empty. +action.fish_on_line=Fish on the line. +action.no_fish_on_line=No fish on the line. +action.cannot_place_snares_in_base=Cannot place snares in the base area. +action.snare_already_here=There is already a snare here. +action.snare_set=Snare set. +action.no_snares_to_place=No snares to place. +action.feed_fire.stick=You dump an arm load of sticks into the fire. +action.feed_fire.vine=You toss a few vines and leaves into the fire. +action.feed_fire.log=You heave a log into the fire. +action.no_altar_built=No altar built. +action.need_clay_pot_for_incense=You need a clay pot to burn incense. +action.no_incense_to_burn=No incense to burn. +action.incense_burning=Incense burning. {hours} hours remaining. +action.no_sticks_to_feed_fire=No sticks to feed fire. +action.dumped_sticks=Dumped {amount} sticks into the fire. +action.no_vines_to_feed_fire=No vines to feed fire. +action.dumped_vines=Dumped {amount} vines into the fire. +action.no_logs_to_feed_fire=No logs to feed fire. +action.dumped_logs=Dumped {amount} logs into the fire. +action.burned_incense=Burned {amount} incense. +{hours} hours. +action.option.place_snare=Place Snare +action.option.feed_fire_stick=Feed fire with stick +action.option.feed_fire_vine=Feed fire with vine +action.option.feed_fire_log=Feed fire with log +action.option.burn_incense=Burn incense +action.option.check_fire=Check fire +action.option.check_snare=Check snare +action.option.check_fishing_pole=Check fishing pole +action.menu.prompt=Action menu. {option} +action.cant_do_that=Can't do that. +base_info.not_in_base=You are not in the base. +base_info.menu.no_options=Base info. No options. +base_info.menu.prompt=Base info. {option} +base.barricade_fallen=The barricade has fallen! +base.no_food_residents_hungry=No food, residents are hungry. +base.resident_fishing_pole_broke_one=A resident's fishing pole broke. +base.resident_fishing_pole_broke_many={count} fishing poles broke. +base.resident_caught_fish_one=Resident caught a fish and added it to storage. +base.resident_caught_fish_many=Residents caught {count} fish and added them to storage. +base.resident_smoked_fish=Resident smoked a fish into {yield} smoked fish. +base.livestock_produced=Livestock produced {meat} meat, {skins} skins, and {feathers} feathers and added to storage. +base.resident_spear_broke_one=A resident's spear broke from wear. +base.resident_spear_broke_many={count} spears broke from wear. +base.resident_sling_broke_one=A resident's sling broke from wear. +base.resident_sling_broke_many={count} slings broke from wear. +base.resident_bow_broke_one=A resident's bow broke from wear. +base.resident_bow_broke_many={count} bows broke from wear. +base.resident_clothing_repair_one=A resident is mending clothing. Used {vines} vines, {skins} skins, and {down} down. +base.resident_clothing_repair_many=Residents are mending clothing. Used {vines} vines, {skins} skins, and {down} down. +base.snare_escape_during_check=A {catch_type} escaped while a resident checked the snare at x {position}. +base.resident_retrieved_from_snare=Resident retrieved {game_type} from snare at x {position} y 0 and reset it. +base.resident_knife_broke_butchering=A resident's knife broke while butchering. +base.resident_butchered_result=Resident butchered {game_type}. Added {meat} meat, {skins} skins, {feathers} feathers, {down} down, {sinew} sinew to storage. +base.resident_added_to_storage=Resident added {item} to storage. +base.resident_basket_broke_one=A resident's basket broke. +base.resident_basket_broke_many={count} baskets broke. +base.resident_basket_broke_foraging_one=A resident's basket broke while foraging. +base.resident_basket_broke_foraging_many={count} baskets broke while foraging. +base.resident_gathered_basket_food_one=Resident gathered a basket of fruits and nuts. +base.resident_gathered_basket_food_many=Residents gathered {count} baskets of fruits and nuts. +fishing.size.small=small +fishing.size.medium=medium sized +fishing.size.large=large +fishing.size.monster=monster +fishing.pole_broke.default=Your fishing pole broke. +fishing.pole_broke.switched_weapons=You switched weapons and your fishing pole broke. +fishing.pole_broke.moved_fish_got_away=You moved and the fish got away. Your fishing pole broke. +fishing.pole_broke.moved=You moved and your fishing pole broke. +fishing.require_pole_equipped=You need a fishing pole equipped. +fishing.require_near_stream=You need to be within 2 tiles of a stream. +fishing.fish_on_line=A fish is on the line! +fishing.caught=Caught a {size} {fish}. +fishing.fish_slipped_off_water=The fish slipped off in the water. +fishing.fish_got_away=The fish got away. +fishing.fish_still_on_line=The fish is still on the line. +fishing.stopped=You stop fishing. +altar.must_be_in_base=Must be in base to use altar. +altar.no_altar_built=No altar built. +altar.runed_cannot_sacrifice=Runed items cannot be sacrificed. +altar.nothing_to_sacrifice=Nothing to sacrifice. +altar.sacrificed_one=Sacrificed 1 {item}. Favor +{gain}. Total {total}. +altar.sacrificed_many=Sacrificed {count} {items}. Favor +{gain}. Total {total}. +altar.menu.prompt=Altar. Favor {favor}. +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. +storage.runed_item_name=Runed {equipment} of {rune} +storage.deposited_one=Deposited {name}. +storage.deposited_many=Deposited {amount} {item}. +storage.withdrew_one=Withdrew {name}. +storage.withdrew_many=Withdrew {amount} {item}. +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. +character.info.name_sex=Name {name}, {sex} +character.info.name_unknown=Name unknown +character.info.health=Health {health} of {max} +character.info.weapon=Weapon {weapon} +character.info.clothing_equipped=Clothing equipped: {items} +character.info.no_clothing=No clothing equipped. +character.info.missing=Missing: {slots} +character.info.favor=Favor {favor} +character.info.speed=Speed {status} +character.info.pet=Pet {pet}, health {health} of {max_health}, loyalty {loyalty} of {max_loyalty} +character.info.pet_knocked_out=Knocked out for {hours} {hour_label}. +character.info.pet_none=No pet. +character.word.hour=hour +character.word.hours=hours +pet.default_name=pet +pet.returned=A {pet} returns to you. +pet.knocked_out=Your {pet} has been knocked out. +pet.knocked_out_with_remaining=Your {pet} has been knocked out. {hours} {hour_label} remaining. +pet.leaves={pet} leaves. +pet.leaving=Your {pet} is leaving. +pet.on_the_way=Your pet is on its way. +pet.hungry_unresponsive=A {pet} is hungry and unresponsive. +pet.joins_you=A {pet} joins you. +pet.offer.prompt=A friendly looking {pet} begs for food. Accept? +pet.offer.declined=Declined. +pet.retrieve.cant_carry_chew_on=You can't carry a {item}, so you give it to a {pet} to chew on. +pet.retrieve.retrieved_item=Your {pet} retrieved {item}. +pet.retrieve.random_find=Your {pet} retrieved {count} {item}. +pet.recovered_from_injuries=Your {pet} has recovered from its injuries. +pet.getting_hungry=A {pet} is getting hungry. +fylgja.already_used_today=You have already used your Fylgja today. +fylgja.menu.prompt=Fylgja menu. +fylgja.need_open_ground_to_charge=You need open ground in front of you to charge. +fylgja.option={name} Fylgja +fylgja.connection_with_target=You have a {stage} connection with the {target}. +fylgja.unlocked=You have unlocked the {name} Fylgja! +fylgja.already_unlocked=You have already unlocked the {name} Fylgja. +fylgja.name.unicorn=Unicorn +fylgja.target.unicorn=unicorn +fylgja.stage.tenuous=tenuous +fylgja.stage.faint=faint +fylgja.stage.stirring=stirring +fylgja.stage.budding=budding +fylgja.stage.kindled=kindled +fylgja.stage.bound=bound +fylgja.stage.sworn=sworn +fylgja.stage.ascendant=ascendant +fylgja.stage.ultimate=ultimate +quest.name.bat_invasion=Bat Invasion +quest.name.catch_boomerang=Catch the Boomerang +quest.name.enchanted_melody=Enchanted Melody +quest.name.escape_from_hel=Escape from Hel +quest.name.skeletal_bard=Skeletal Bard +quest.name.unknown=Unknown Quest +quest.description.bat_invasion=Bat Invasion. Giant killer bats are attacking. Press space to throw when the bat is centered. +quest.description.catch_boomerang=Catch the Boomerang. Throw and catch the boomerang as it returns for up to 4 points. +quest.description.enchanted_melody=Enchanted Melody. Repeat the pattern using E R D F or U I J K. Lowest to highest pitch. +quest.description.escape_from_hel=Escape from Hel. Press space to jump over open graves. The pace quickens. +quest.description.skeletal_bard=Skeletal Bard. A skeleton named Billy Bones is practicing to become a bard. Count the notes. +quest.description.unknown=Unknown quest. +quest.new_available=A new quest is available: {quest}. +quest.none_available=No quests available. +quest.favor_phrase.displeased=The gods are displeased with your performance. +quest.favor_phrase.pleased=The gods are pleased with your performance. +quest.favor_phrase.very_pleased=The gods are very pleased with your performance. +quest.favor_phrase.extremely_pleased=The gods are extremely pleased with your performance. +quest.favor_phrase.ultimately_pleased=The gods are ultimately pleased with your performance. +quest.rewards.title=Quest Rewards +quest.rewards.complete_heading=Quest Complete! +quest.rewards.heading=Rewards: +quest.rewards.favor_line=Favor: +{favor} +quest.rewards.item_line={item}: +{amount} +quest.rewards.inventory_full_line=Inventory full, could not receive {item}. +quest.rewards.no_items_awarded=No items awarded. +quest.rewards.score_line=Score: {score} +quest.menu.prompt=Quest menu. +quest.menu.count={index} of {total} +quest.minigame.starting=Starting. +quest.bat_invasion.complete_score=Bat invasion complete. Score {score}. +quest.boomerang.press_space_to_throw=Press Space to throw. +quest.boomerang.skill.almost_dropped=almost dropped +quest.boomerang.skill.managed_to_catch=managed to catch +quest.boomerang.skill.caught=caught +quest.boomerang.skill.expertly_caught=expertly caught +quest.boomerang.caught_result=You {skill} the boomerang. {points} points.{suffix} +quest.boomerang.missed=You missed the boomerang.{suffix} +quest.boomerang.hit_you=The boomerang hit you.{suffix} +quest.boomerang.summary=You caught {count} boomerangs for a total of {score} points. +quest.enchanted_melody.practice_prompt=Practice mode. Press Enter to begin, Escape to cancel. +quest.enchanted_melody.starting_prompt=Starting. Repeat the pattern. +quest.enchanted_melody.score=You matched {rounds} notes. Score {score}. +quest.skeletal_bard.select_notes=Select number of notes. {selection}. +quest.skeletal_bard.round_result=You entered {guess} notes and the actual number was {actual}. {points} points. Press Enter to continue. +quest.skeletal_bard.complete_score=Skeletal Bard complete. Your score for all five tunes is {score}. +quest.escape_from_hel.fell_in_score=You fell in. Score {score}. +adventure.none_in_base=No adventures available in the base. +adventure.already_attempted_today=You have already attempted an adventure today. +adventure.option.unicorn_hunt=Unicorn Hunt (Mountain Boss) +adventure.option.bandit_hideout=Bandit's Hideout +adventure.none_in_area=No adventures found in this area. +adventure.menu.prompt=Adventure menu. +adventure.victory=Victory! +adventure.rewards.title==== Victory Rewards === +adventure.unicorn.flee=You fled from the unicorn. +adventure.unicorn.position_report=X {x}, {terrain}, facing {direction} +adventure.unicorn.player_trampled=The unicorn trampled you! +adventure.unicorn.victory_title=Unicorn Victory +adventure.unicorn.reward.favor_awarded=The gods are pleased with your victory! {favor} favor awarded. +adventure.unicorn.reward.heal_scrolls=Heal Scrolls: +{count}. +adventure.unicorn.reward.learned_rune_swiftness=Learned Rune of Swiftness! +adventure.unicorn.reward.engrave_unlocked=You can now engrave equipment with this rune at the crafting menu. +adventure.unicorn.reward.mastered_rune_swiftness=You have already mastered the Rune of Swiftness. +adventure.unicorn.horses.no_stable_or_storage=Stable or storage not built. No horses were captured. +adventure.unicorn.horses.stable_full=Stable is full. No horses were captured. +adventure.unicorn.horses.joined_stable=A horse joins your stable. +adventure.unicorn.horses.none_captured=No horses were captured. +adventure.bandit_hideout.flee=You fled from the bandit hideout. +adventure.bandit_hideout.position_report=X {x}, {terrain}. Distance to base {distance}. Facing {direction}. Base health {health} of {max}. +adventure.bandit_hideout.player_defeated=You were defeated in the bandit hideout. +character.report.position={direction}, x {x}, y {y}, terrain {terrain} +character.report.terrain_mountains={terrain}. Mountains +character.report.player={health} of {max} health +character.report.pet={pet}: {health} of {max} health, loyalty {loyalty} of {max_loyalty} +item.label.items=items +item.label.item=item +item.label.unknown=Unknown +equipment.menu.prompt=Equipment menu. {option} +equipment.menu.nothing_to_equip=Nothing to equip. +equipment.menu.equipped_suffix= (equipped) +equipment.set_to_slot={name} set to slot {slot}. +equipment.equipped={name} equipped. +equipment.unequipped={name} unequipped. +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. +environment.fall.damage_report=Fell {height} feet! Took {damage} damage. {health} health remaining. +environment.snare.collected_with_catch=Collected {catch_type} and snare. +environment.snare.collected_empty=Collected snare. +environment.tree.cut_down=This tree has been cut down. +environment.tree.empty=This tree is empty. +environment.area.nothing_left=This area has nothing left. +environment.tree.climb_started=Started climbing tree. Height is {height} feet. +environment.tree.climb_reached_top=Reached the top at {height} feet. +environment.tree.climb_reached_ground=Safely reached the ground. +environment.tree.climb_down=Climbing down. +environment.movement.need_rope=You'll need a rope to climb there. +environment.movement.press_up_climb=Press up to climb up. +environment.movement.press_down_climb=Press down to climb down. +environment.movement.need_canoe=You need a canoe to cross deep water. +environment.rope_climb.direction.up=up +environment.rope_climb.direction.down=down +environment.rope_climb.progress=Climbing {direction}. {distance} feet. +environment.rope_climb.reached_elevation=Reached elevation {elevation}. +snare.escape_at=A {catch_type} escaped from your snare at x {position} y 0! +snare.caught_at={catch_type} caught in snare at x {position} y 0! +snare.stepped_on_with_escape=You stepped on your snare! The {catch_type} escaped. +snare.stepped_on_broke=You stepped on your snare and broke it! +fire.low_fuel_at=Fire at x {position} y {y} is getting low! +fire.went_out_at=Fire at x {position} y {y} has gone out. +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.in_progress=Crafting... +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.weapons.crafted.spear=Crafted a Spear. +crafting.weapons.crafted.sling=Crafted a Sling. +crafting.weapons.crafted.bow=Crafted a Bow. +crafting.weapons.crafted.stone_axe=Crafted a Stone Axe. +crafting.weapons.crafted_max.spears=Crafted {count} Spears. +crafting.weapons.crafted_max.slings=Crafted {count} Slings. +crafting.weapons.crafted_max.bows=Crafted {count} Bows. +crafting.weapons.crafted_max.stone_axes=Crafted {count} Stone Axes. +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.tools.crafted.stone_knife=Crafted a Stone Knife. +crafting.tools.crafted.snare=Crafted a Snare. +crafting.tools.crafted.fishing_pole=Crafted a Fishing Pole. +crafting.tools.crafted.rope=Crafted rope. +crafting.tools.crafted.quiver=Crafted a Quiver. +crafting.tools.crafted.canoe=Crafted a Canoe. +crafting.tools.crafted.reed_basket=Crafted a reed basket. +crafting.tools.crafted.clay_pot=Crafted a clay pot. +crafting.tools.crafted_max.stone_knives=Crafted {count} Stone Knives. +crafting.tools.crafted_max.snares=Crafted {count} Snares. +crafting.tools.crafted_max.fishing_poles=Crafted {count} Fishing Poles. +crafting.tools.crafted_max.ropes=Crafted {count} Rope. +crafting.tools.crafted_max.quivers=Crafted {count} Quivers. +crafting.tools.crafted_max.canoes=Crafted {count} Canoes. +crafting.tools.crafted_max.reed_baskets=Crafted {count} Reed Baskets. +crafting.tools.crafted_max.clay_pots=Crafted {count} Clay Pots. +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.materials.not_enough_quiver_capacity_for_arrows=Not enough quiver capacity for arrows. +crafting.materials.crafted.arrows=Crafted {count} arrows. +crafting.materials.crafted.bowstring=Crafted a bowstring. +crafting.materials.crafted_max.bowstrings=Crafted {count} Bowstrings. +crafting.materials.crafted.incense=Crafted incense. +crafting.materials.crafted_max.incense=Crafted {count} Incense. +crafting.materials.smoke_fish.crafted=Smoked a fish into {yield} smoked fish. +crafting.materials.smoke_fish.crafted_max=Smoked {count} fish into {yield} smoked fish. +crafting.materials.butcher.goose=Butchered goose. Got 1 meat, feathers, and down. +crafting.materials.butcher.turkey=Butchered turkey. Got 1 meat and feathers. +crafting.materials.butcher.boar=Butchered boar. Got meat, 3 skins, and 2 sinew. +crafting.materials.butcher.default=Butchered {game}. Got 1 meat and 1 skin. +crafting.materials.butcher.no_space_outputs=No space for outputs. +crafting.materials.butcher.max_result=Butchered {count} game. Got {meat} meat, {skins} skins, {feathers} feathers, {down} down, {sinew} sinew. +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.clothing.crafted.skin_hat=Crafted a Skin Hat. +crafting.clothing.crafted.skin_gloves=Crafted Skin Gloves. +crafting.clothing.crafted.skin_pants=Crafted Skin Pants. +crafting.clothing.crafted.skin_tunic=Crafted a Skin Tunic. +crafting.clothing.crafted.moccasins=Crafted moccasins. +crafting.clothing.crafted.skin_pouch=Crafted a Skin Pouch. +crafting.clothing.crafted.backpack=Crafted a Backpack. +crafting.clothing.crafted_max.skin_hats=Crafted {count} Skin Hats. +crafting.clothing.crafted_max.skin_gloves=Crafted {count} Skin Gloves. +crafting.clothing.crafted_max.skin_pants=Crafted {count} Skin Pants. +crafting.clothing.crafted_max.skin_tunics=Crafted {count} Skin Tunics. +crafting.clothing.crafted_max.moccasins=Crafted {count} Moccasins. +crafting.clothing.crafted_max.skin_pouches=Crafted {count} Skin Pouches. +crafting.clothing.crafted_max.backpacks=Crafted {count} Backpacks. +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.buildings.none_available=No buildings available. +crafting.buildings.firepit.already_in_base=There is already a firepit in the base. +crafting.buildings.firepit.already_here=There is already a firepit here. +crafting.buildings.firepit.built=Firepit built here. +crafting.buildings.fire.requires_firepit_nearby=You need a firepit within 2 tiles to build a fire. +crafting.buildings.fire.already_in_base=There is already a fire in the base. +crafting.buildings.fire.built=Fire built at firepit. +crafting.buildings.herb_garden.base_only=Herb garden can only be built in the base area. +crafting.buildings.herb_garden.already_in_base=There is already an herb garden in the base. +crafting.buildings.herb_garden.built=Herb garden built. The base now heals faster. +crafting.buildings.storage.base_only=Storage must be built in the base. +crafting.buildings.storage.fully_upgraded=Storage is fully upgraded. +crafting.buildings.storage.upgraded=Storage upgraded. Capacity is now {capacity} per item. +crafting.buildings.pasture.base_only=Pasture must be built in the base. +crafting.buildings.pasture.requires_storage_upgrade=Storage must be upgraded before a pasture. +crafting.buildings.pasture.already_built=Pasture already built. +crafting.buildings.pasture.built=Pasture built. +crafting.buildings.stable.base_only=Stable must be built in the base. +crafting.buildings.stable.requires_storage_upgrade=Storage must be upgraded before a stable. +crafting.buildings.stable.already_built=Stable already built. +crafting.buildings.stable.built=Stable built. +crafting.buildings.altar.base_only=Altar must be built in the base. +crafting.buildings.altar.already_built=Altar already built. +crafting.buildings.altar.built=Altar built. +crafting.runes.base_only=Rune engraving can only be done in the base area. +crafting.runes.option={rune} (1 Clay, 1 Favor) [Requires Knife] +crafting.runes.none_unlocked=No runes unlocked yet. +crafting.runes.prompt=Runes. {option} +crafting.runes.equipment_option={name} ({count} available) +crafting.runes.no_equipment_available=No equipment available to engrave. +crafting.runes.select_equipment_prompt=Select equipment to engrave with {rune}. {option} +crafting.runes.no_item_available=No {item} available. +crafting.runes.runed_name=Runed {equipment} of {rune} +crafting.runes.engraved=Engraved {item}. +crafting.runes.engraved_max=Engraved {count} {item} with {rune}. +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.barricade.full_health=Barricade is already at full health. +crafting.barricade.no_materials=No materials to reinforce the barricade. +crafting.barricade.reinforced.sticks=Reinforced barricade with sticks. +{gained} health. Now {health} of {max}. +crafting.barricade.reinforced.vines=Reinforced barricade with vines. +{gained} health. Now {health} of {max}. +crafting.barricade.reinforced.log=Reinforced barricade with log. +{gained} health. Now {health} of {max}. +crafting.barricade.reinforced.stones=Reinforced barricade with stones. +{gained} health. Now {health} of {max}. +crafting.barricade.reinforced_max.sticks=Reinforced barricade {count} times with sticks. Health now {health}. +crafting.barricade.reinforced_max.vines=Reinforced barricade {count} times with vines. Health now {health}. +crafting.barricade.reinforced_max.log=Reinforced barricade {count} times with log. Health now {health}. +crafting.barricade.reinforced_max.stones=Reinforced barricade {count} times with stones. Health now {health}. +crafting.barricade.not_enough.sticks=Not enough sticks. +crafting.barricade.not_enough.vines=Not enough vines. +crafting.barricade.not_enough.logs=Not enough logs. +crafting.barricade.not_enough.stones=Not enough stones. +crafting.missing=Missing: {requirements} +crafting.requirement.stone_knife=Stone Knife +crafting.requirement.game=Game +crafting.requirement.favor=favor +crafting.requirement.resources=resources +story.voice.format={speaker}: {line} +story.god.fallback=A god +story.god.odin=Odin +story.god.thor=Thor +story.god.freyja=Freyja +story.god.loki=Loki +story.god.tyr=Tyr +story.god.baldur=Baldur +story.god.frigg=Frigg +story.god.heimdall=Heimdall +story.god.hel=Hel +story.god.fenrir=Fenrir +story.god.freyr=Freyr +story.line01=The world has fallen. +story.line02=Once-great cities lie in ruin. +story.line03=In most places, not two stones remain as mankind set them. +story.line04=The gods, long thought sleeping, achieved the impossible. +story.line05=Ragnarok was averted. +story.line06=Great Fenrir was freed and did not strike down his captors. +story.line07=Surtr did not lay waste to our world with his flaming greatsword. +story.line08=For a fleeting age, the gods wrestled fate into silence. +story.line09=But the end of the world was delayed, never denied. +story.line10=One force lay beyond even divine command: mankind. +story.line11=Hatred swelled. +story.line12=Greed answered it. +story.line13=Mercy withered. +story.line14=Differences became crimes. +story.line15=Folk no longer argued; they tore at one another like beasts. +story.line16=Neighbors became enemies for trifles: a color, a song, a single word. +story.line17=War followed. +story.line18=Cities burned, roads broke, and monsters wearing human faces roamed the streets. +story.line19=Then ruin deepened into terror. +story.line20=The dead, enraged by wanton slaughter, rose from their graves. +story.line21=Corpses woke and hunted the living. +story.line22=Night-stalkers fed on blood. +story.line23=Even ash and dust remembered form and rose again. +story.line24=Soon all that remained was rubble and bone. +story.line25=Your last memory is flight from a vast horde of nightmare creatures. +story.line26=When you wake, you are somewhere strange. +story.line27=A crude wooden shelter stands against the wind, open to the elements, braced with sticks and logs. +story.line28=As your senses return, faint voices stir in the dark. +story.line29=After the death of this world, another shall rise. +story.line30=You have been judged worthy to try and survive. +story.line31=This world belongs to the dead. +story.line32=In time, they will slay even us. +story.line33=Prove yourself, warrior, and you may walk into the next world. +story.line34=May your death, when it comes, be honorable. +story.line35=We will aid you while we still have strength, but time is against us. +story.line36=Beyond your shelter lies grassland, and farther off, broken stone that may once have been a city. +story.line37=In the total devastation, even memory has lost its shape. + +[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/item_registry.nvgt:item_plural:backpacks +msg.03268c7d7239=backpacks +; src/item_registry.nvgt:item_singular:rope +msg.03976920927e=rope +; src/item_registry.nvgt:item_display:small_game +msg.0406529811c7=Small Game +; draugnorak.nvgt:608: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/bosses/bandit_hideout.nvgt:787:insert_last[0] +msg.066723a92e94==== Victory Rewards === +; 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/sound_settings.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/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/bosses/bandit_hideout.nvgt:305:insert_last[0] +msg.0b3d8d5cdca6=- Reach the base and destroy the barricade +; seed:learn_sounds_label:sounds/terrain/forest.ogg +msg.0bcd9af79f2d=forest +; seed:learn_sounds_label:sounds/crafting_complete.ogg +msg.0c7789cf9a52=crafting complete +; src/item_registry.nvgt:item_display:arrows +msg.0cf604cb001b=Arrows +; seed:learn_sounds_label:sounds/weapons/sling_swing.ogg +msg.0d1ec2e04286=sling swing +; src/item_registry.nvgt:item_plural:knives +msg.0e16a11cda9a=knives +; src/item_registry.nvgt:item_singular:spear +msg.0e57fb642209=spear +; src/item_registry.nvgt:item_display:slings +msg.0eb796e65c39=Slings +; src/item_registry.nvgt:item_singular:skin_pouch +msg.0f9af12cbb82=skin pouch +; src/item_registry.nvgt:item_display:logs +msg.126dd3b70a5c=Logs +; libstorm-nvgt/file_viewer.nvgt:74:screen_reader_speak[0] +msg.136e4cb03bd5=Failed to save file +; src/item_registry.nvgt:item_plural:quivers +msg.145ba8a7a0f2=quivers +; src/bosses/bandit_hideout.nvgt:860:insert_last[0] +msg.14f5f5ff4f76=No survivors were found. +; seed:learn_sounds_label:sounds/game/game_falls.ogg +msg.16134a4250f7=game falls +; src/quests/escape_from_hel_game.nvgt:12:insert_last[0] +msg.16a233f44489=- Listen for open graves approaching (growing louder) +; seed:manual +msg.176b9a0c8398=Really exit? +; src/item_registry.nvgt:item_singular:incense_stick +msg.17808e3775e1=incense stick +; src/quests/skeletal_bard_game.nvgt:44:insert_last[0] +msg.17b5b2a286cc==== Skeletal Bard === +; seed:learn_sounds_label:sounds/quests/bat1.ogg +msg.18b837bb6f43=bat1 +; seed:learn_sounds_label:sounds/enemies/yeti_dies.ogg +msg.18c77e824a2a=yeti dies +; seed:learn_sounds_label:sounds/enemies/bandit_dies.ogg +msg.1a853cbfbc36=bandit dies +; src/sound_settings.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/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/bosses/bandit_hideout.nvgt:806: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/item_registry.nvgt:item_singular:axe +msg.237b5017397b=axe +; src/bosses/unicorn/unicorn_boss.nvgt:117:insert_last[0] +msg.245c89590526=- When the bridge collapses with the Unicorn on it, you win! +; src/bosses/bandit_hideout.nvgt:834: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 +; 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/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:52: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:817: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/quests/bat_invasion_game.nvgt:19:text_reader_lines[1] +; src/quests/catch_the_boomerang_game.nvgt:26:text_reader_lines[1] +; src/quests/enchanted_melody_game.nvgt:39:text_reader_lines[1] +msg.29f291640516=Quest Instructions +; src/quests/escape_from_hel_game.nvgt:11:insert_last[0] +msg.2afb87ae1752=- You run automatically, speed increases over time +; src/sound_settings.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 +; seed:learn_sounds_label:sounds/weapons/spear_swing.ogg +msg.2e094dd0a2ab=spear swing +; src/item_registry.nvgt:item_display:fish +msg.3030e8ec7633=Fish +; src/quests/skeletal_bard_game.nvgt:50:insert_last[0] +msg.33bc40d8201e=- After the tune, choose the number of notes +; 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/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/item_registry.nvgt:item_plural:skins +msg.37423f3bd00f=skins +; 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/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/item_registry.nvgt:item_plural:skin_pouches +msg.3c32451c9c71=skin pouches +; seed:learn_sounds_label:sounds/actions/fishpole.ogg +msg.3cebb4c50866=fishpole +; seed:learn_sounds_label:sounds/terrain/hard_stone.ogg +msg.3e024b87e650=hard stone +; src/item_registry.nvgt:item_display:skins +msg.3e03229d39e4=Skins +; src/item_registry.nvgt:item_plural:skin_gloves +msg.3eb9cd52ec8d=skin gloves +; src/quests/enchanted_melody_game.nvgt:34:insert_last[0] +msg.3f638a51cb47=E or U - Fourth note (highest pitch) +; 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/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/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} +; seed:learn_sounds_label:sounds/quests/spear_miss.ogg +msg.437ecef821db=spear miss +; src/item_registry.nvgt:item_plural:bowstrings +msg.43821cae64c5=bowstrings +; seed:learn_sounds_label:sounds/actions/falling.ogg +msg.444d4678962b=falling +; seed:learn_sounds_label:sounds/weapons/bow_fire.ogg +msg.4537c16badd9=bow fire +; src/item_registry.nvgt:item_plural:snares +msg.455b11a16eb7=snares +; src/sound_settings.nvgt:38:learn_sounds_add_description[1] +msg.4671b16b0398=Not quite a zombie nor a vampyr. These undead remember they used to have a home, and maybe there's food there... +; seed:learn_sounds_label:sounds/terrain/draugrhaugr_step.ogg +msg.46fcdd91c99e=draugrhaugr step +; src/item_registry.nvgt:item_plural:logs +msg.474c797713f3=logs +; src/item_registry.nvgt:item_display:snares +msg.483a17dd2a17=Snares +; 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/item_registry.nvgt:item_plural:stones +msg.4c0d2469f78b=stones +; seed:learn_sounds_label:sounds/notify.ogg +msg.4f08301dcb7b=notify +; seed:learn_sounds_label:sounds/terrain/shallow_water.ogg +msg.4f11c6797b8d=shallow water +; 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 +; seed:learn_sounds_label:sounds/game/turkey.ogg +msg.5009c8e190ee=turkey +; src/quests/skeletal_bard_game.nvgt:51: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 +; draugnorak.nvgt:81:insert_last[0] +msg.528fb1c4e5fb=New Game +; seed:learn_sounds_label:sounds/quests/squish.ogg +msg.5460f2e3c43e=squish +; seed:learn_sounds_label:sounds/actions/fishpole_break.ogg +msg.546330ee64db=fishpole break +; draugnorak.nvgt:608:ui_info_box[0] +msg.54fc2d657ed6=Draugnorak +; seed:learn_sounds_label:sounds/enemies/vampyr3.ogg +msg.55748cf43121=vampyr3 +; src/quests/bat_invasion_game.nvgt:9:insert_last[0] +msg.5665ef30ef4d=Bats are invading! Throw your spear to defend. +; 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. +; seed:learn_sounds_label:sounds/enemies/wight_dies.ogg +msg.5b61e4982b59=wight dies +; src/item_registry.nvgt:item_display:feathers +msg.5bdb7fa2223d=Feathers +; src/item_registry.nvgt:item_plural:reeds +msg.5c2434ecd90f=reeds +; src/bosses/bandit_hideout.nvgt:829: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. +; seed:learn_sounds_label:sounds/actions/cast_strength.ogg +msg.5dfc869b7588=cast strength +; src/time_system.nvgt:793: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/menus/base_info.nvgt:12:insert_last[0] +msg.623f78278373=Barricade health {arg1} of {arg2} +; src/bosses/bandit_hideout.nvgt:827: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 +; 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/item_registry.nvgt:item_plural:skin_pants +; src/item_registry.nvgt:item_singular:skin_pants +msg.65eb0fb6ab40=skin pants +; 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:21:insert_last[0] +msg.6861fd95f4f7=- There are 5 turns +; 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 +; seed:learn_sounds_label:sounds/actions/hit_ground.ogg +msg.704e634dd69f=hit ground +; seed:learn_sounds_label:sounds/actions/set_snare.ogg +msg.714165bec103=set snare +; src/quests/skeletal_bard_game.nvgt:53: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] +msg.731152a028c6=Closed. +; src/quests/catch_the_boomerang_game.nvgt:20:insert_last[0] +msg.7680f62917b1=- Press Space again when it sounds close +; 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/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/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/quests/skeletal_bard_game.nvgt:47: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/sound_settings.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:194:ui_info_box[1] +; draugnorak.nvgt:208: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/time_system.nvgt:775:notify[0] +; src/time_system.nvgt:785:notify[0] +msg.81df04a81e24={arg1} favor shines upon you. {arg2} +; src/quests/catch_the_boomerang_game.nvgt:22:insert_last[0] +msg.823be948275f=- Better timing earns more points (up to 4) +; src/sound_settings.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. +; 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! +; 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 +; seed:learn_sounds_label:sounds/quests/bone5.ogg +msg.86290fe7e711=bone5 +; seed:learn_sounds_label:sounds/dialog.ogg +msg.863be329d064=dialog +; 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/time_system.nvgt:789:notify[0] +msg.8818f1d55166={arg1} radiance fills residents with purpose. +; 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/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:16: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! +; seed:learn_sounds_label:sounds/quests/bee.ogg +msg.917ee46db0cd=bee +; seed:learn_sounds_label:sounds/player_male_damage.ogg +msg.924e0eb6b6f1=player male damage +; src/time_system.nvgt:780:notify[0] +msg.9281d2fe5f08={arg1} favor shines upon you. You feel swift for a while. +; seed:learn_sounds_label:sounds/actions/climb_rope.ogg +msg.92abc6d6953f=climb rope +; src/bosses/bandit_hideout.nvgt:800:insert_last[0] +msg.9379874d592b=Altar not built. No favor awarded. +; src/bosses/unicorn/unicorn_boss.nvgt:113:insert_last[0] +msg.93894d088f6a=- Use your axe to destroy a bridge support +; src/item_registry.nvgt:item_singular:stick +; seed:learn_sounds_label:sounds/items/stick.ogg +msg.946466f6c963=stick +; seed:learn_sounds_label:sounds/game/boar.ogg +msg.957f554c342a=boar +; src/bosses/bandit_hideout.nvgt:857:insert_last[0] +msg.97c59037a63c=A survivor joins your base. +; src/item_registry.nvgt:item_plural:smoked_fish +; src/item_registry.nvgt:item_singular:smoked_fish +msg.995d4e06ae4e=smoked fish +; src/sound_settings.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:19:insert_last[0] +msg.9afdb7e61391=- Press Space to throw +; src/bosses/bandit_hideout.nvgt:865:text_reader_lines[1] +msg.9b64062b2074=Bandit's Hideout +; 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/quests/bat_invasion_game.nvgt:11:insert_last[0] +; src/quests/catch_the_boomerang_game.nvgt:18:insert_last[0] +; src/quests/escape_from_hel_game.nvgt:10:insert_last[0] +msg.a040b931b9bb=How to play: +; 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/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 +; src/sound_settings.nvgt:43:learn_sounds_add_description[1] +; src/sound_settings.nvgt:44:learn_sounds_add_description[1] +; src/sound_settings.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:798:insert_last[0] +msg.a646f0dcb735=Favor awarded: {arg1}. +; 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/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 +; 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 +; seed:manual +msg.abe73973cc7c=Unable to load save. +; src/quests/enchanted_melody_game.nvgt:30:insert_last[0] +msg.ac36af678d78=Controls: +; src/item_registry.nvgt:item_singular:moccasin +msg.ad34f00c46cd=moccasin +; seed:learn_sounds_label:sounds/enemies/goblin_dies.ogg +msg.ad36d64ad3a3=goblin dies +; 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/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 +; 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/item_registry.nvgt:item_display:skin_tunics +msg.b48d0661f0da=Skin Tunics +; seed:learn_sounds_label:sounds/enemies/bandit3.ogg +msg.b5d5cc084a95=bandit3 +; seed:learn_sounds_label:sounds/actions/call_pet.ogg +msg.b73a0709297f=call pet +; src/item_registry.nvgt:item_display:knives +msg.b774102c89b5=Knives +; seed:learn_sounds_label:sounds/terrain/snow.ogg +msg.b94e9f3d7e00=snow +; src/bosses/bandit_hideout.nvgt:844: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/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 +; seed:learn_sounds_label:sounds/weapons/arrow_hit.ogg +msg.bdebda25dad9=arrow hit +; src/item_registry.nvgt:item_display:down +msg.bf93e5ce8bc1=Down +; src/sound_settings.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:821:insert_last[0] +msg.c05feaca7269=No items were recovered. +; src/item_registry.nvgt:item_singular:snare +msg.c1104d82c0bd=snare +; src/sound_settings.nvgt:37:learn_sounds_add_description[1] +msg.c48d642737b2=Inhabits mountainous regions. +; src/item_registry.nvgt:item_display:smoked_fish +msg.c4f7e9c21601=Smoked Fish +; src/sound_settings.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. +; seed:learn_sounds_label:sounds/quests/fall.ogg +msg.c52888225c69=fall +; seed:learn_sounds_label:sounds/enemies/bandit1.ogg +msg.c65e5dfc5ba7=bandit1 +; src/bosses/bandit_hideout.nvgt:789: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/sound_settings.nvgt:33:learn_sounds_add_description[1] +msg.c8da1c4f1714=Lowers in pitch as the fall progresses. +; src/item_registry.nvgt:item_display:meat +msg.c90399f41b3f=Meat +; seed:learn_sounds_label:sounds/quests/bone7.ogg +msg.c9532ab465fb=bone7 +; src/bosses/bandit_hideout.nvgt:808: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/item_registry.nvgt:item_plural:reed_baskets +msg.cc61d0f3de65=reed baskets +; seed:learn_sounds_label:sounds/weapons/axe_hit.ogg +msg.cccc5c842adb=axe hit +; 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:316:insert_last[0] +msg.ce3897c6b3be={arg1}: {arg2} +; seed:learn_sounds_label:sounds/items/item_breaks.ogg +msg.ce5aeca95fb9=item breaks +; src/i18n.nvgt:677:speak_with_history[0] +; src/pet_system.nvgt:926: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 +; 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:608: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 +; seed:manual +msg.d609f615ddae=No saves found. +; 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:836:insert_last[0] +msg.d7879bd506af=No livestock were recovered. +; 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/sound_settings.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/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 +; 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:46: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/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/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/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 +; seed:learn_sounds_label:sounds/actions/bad_cast.ogg +msg.e34161b98188=bad cast +; 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:24:insert_last[0] +; src/quests/escape_from_hel_game.nvgt:17:insert_last[0] +msg.e4427f09bc09=Close this screen to begin. +; 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:847: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 +; 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:96:insert_last[0] +msg.e8db3a42f86f=Learn Sounds +; src/bosses/bandit_hideout.nvgt:845:insert_last[0] +msg.e8e8f4ee7a5a=You can now engrave weapons with this rune at the crafting menu. +; src/bosses/bandit_hideout.nvgt:302:insert_last[0] +msg.e9dd1d268b65=The base lies far to the east, guarded by a barricade. +; src/menus/base_info.nvgt:36:insert_last[0] +msg.ea771032e597=Altar built +; libstorm-nvgt/notifications.nvgt:153:notifications_speak[0] +; libstorm-nvgt/notifications.nvgt:173:notifications_speak[0] +msg.ebf229b227f8={arg1} {arg2} of {arg3} +; 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/item_registry.nvgt:item_display:ropes +msg.f12b2d219fe8=Ropes +; src/quests/catch_the_boomerang_game.nvgt:14:insert_last[0] +msg.f305da2c3550==== Catch the Boomerang === +; 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/item_registry.nvgt:item_singular:reed +msg.f5cabf873590=reed +; src/bosses/bandit_hideout.nvgt:301:insert_last[0] +msg.f77fc018cb7b=You find a hidden bandit base deep in the forest. +; draugnorak.nvgt:112:insert_last[0] +msg.f83b6fe3aebf=Exit +; src/inventory_items.nvgt:460:speak_with_history[0] +; src/quests/skeletal_bard_game.nvgt:26:speak_with_history[0] +; src/quests/skeletal_bard_game.nvgt:32:speak_with_history[0] +msg.f85b4b604c9b={arg1}. +; seed:learn_sounds_label:sounds/enemies/undead_resident_dies.ogg +msg.f8909d26fe86=undead resident dies +; seed:manual +msg.f8fc573e2d2f=Load Game (no saves found) +; src/item_registry.nvgt:item_singular:bow +msg.fa5bd38090c7=bow +; 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/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}. +; seed:learn_sounds_label:sounds/menu/Draugnorak.ogg +; seed:learn_sounds_label:sounds/menu.bak/Draugnorak.ogg +msg.fe2a9d6606e0=draugnorak +; 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 diff --git a/lang/es.ini b/lang/es.ini new file mode 100644 index 0000000..1720f42 --- /dev/null +++ b/lang/es.ini @@ -0,0 +1,969 @@ +[meta] +code=es +name=Spanish +native_name=Espanol + +[system] +sex.male=Masculino +sex.female=Femenino +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. +load_game.save_deleted=Partida eliminada. +load_game.no_saves_found=No hay partidas guardadas. +load_game.unable_to_load=No se pudo cargar la partida. +load_game.select_prompt=Cargar partida. Selecciona personaje. Presiona Suprimir para borrar una partida. +main_menu.prompt=Draugnorak. Menu principal. +game.new_game_started=Partida nueva iniciada. +game.loaded=Partida cargada. +game.paused_prompt=Pausado. Pulsa retroceso para reanudar. +game.resumed=Reanudado. +exit.confirm=Salir de verdad? +direction.west=oeste +direction.east=este +inventory.cant_carry_any_more_item=No puedes llevar mas {item}. +menu.main.story=La historia +story.voice.format={speaker}: {line} +story.god.fallback=Un dios +story.god.odin=Odin +story.god.thor=Thor +story.god.freyja=Freyja +story.god.loki=Loki +story.god.tyr=Tyr +story.god.baldur=Baldur +story.god.frigg=Frigg +story.god.heimdall=Heimdall +story.god.hel=Hel +story.god.fenrir=Fenrir +story.god.freyr=Freyr +story.line01=El mundo ha caido. +story.line02=Las ciudades que una vez fueron grandes yacen en ruinas. +story.line03=En la mayoria de los lugares, no quedan dos piedras como las coloco la humanidad. +story.line04=Los dioses, que se creian dormidos, lograron lo imposible. +story.line05=Ragnarok fue evitado. +story.line06=El gran Fenrir fue liberado y no derribo a sus captores. +story.line07=Surtr no redujo nuestro mundo a cenizas con su gran espada en llamas. +story.line08=Durante una era fugaz, los dioses obligaron al destino a guardar silencio. +story.line09=Pero el fin del mundo solo se retraso; nunca se nego. +story.line10=Una fuerza estaba mas alla incluso del mandato divino: la humanidad. +story.line11=El odio crecio. +story.line12=La codicia respondio. +story.line13=La misericordia se marchito. +story.line14=Las diferencias se volvieron crimenes. +story.line15=La gente ya no discutia; se despedazaba como bestias. +story.line16=Los vecinos se hicieron enemigos por nimiedades: un color, una cancion, una sola palabra. +story.line17=La guerra siguio. +story.line18=Las ciudades ardieron, los caminos se rompieron y monstruos con rostro humano vagaron por las calles. +story.line19=Entonces la ruina se volvio terror. +story.line20=Los muertos, enfurecidos por la matanza sin sentido, se alzaron de sus tumbas. +story.line21=Los cadaveres despertaron y cazaron a los vivos. +story.line22=Acechadores nocturnos se alimentaron de sangre. +story.line23=Incluso la ceniza y el polvo recordaron su forma y se alzaron otra vez. +story.line24=Pronto, todo lo que quedaba era escombro y hueso. +story.line25=Tu ultimo recuerdo es huir de una inmensa horda de criaturas de pesadilla. +story.line26=Cuando despiertas, estas en un lugar extrano. +story.line27=Un refugio de madera, tosco y abierto al clima, resiste el viento, reforzado con palos y troncos. +story.line28=Cuando tus sentidos regresan, voces tenues se agitan en la oscuridad. +story.line29=Despues de la muerte de este mundo, otro se alzara. +story.line30=Has sido juzgado digno de intentar sobrevivir. +story.line31=Este mundo pertenece a los muertos. +story.line32=Con el tiempo, ellos nos mataran incluso a nosotros. +story.line33=Demuestrate digno, guerrero, y podras caminar hacia el proximo mundo. +story.line34=Que tu muerte, cuando llegue, sea honorable. +story.line35=Te ayudaremos mientras aun tengamos fuerzas, pero el tiempo esta en nuestra contra. +story.line36=Mas alla de tu refugio hay praderas, y mas lejos, piedra rota que alguna vez pudo ser una ciudad. +story.line37=En la devastacion total, incluso la memoria perdio su forma. +language.select_prompt=Selecciona tu idioma. +language.selected=Idioma establecido en {language}. +language.english_label=English (en) +ui.window_title=Draugnorak +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? +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.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. +inventory.max_arrows_with_quivers=Solo puedes llevar {max} flechas con tus carcajes actuales. +inventory.item_not_available=Objeto no disponible. +search.found_item=Encontraste {item}. +search.found_nothing=No encontraste nada. +pickup.item=Recogiste {item}. +time.period.am=a. m. +time.period.pm=p. m. +time.report={hour} en punto {period} dia {day} +time.report_with_conditions={time}. {conditions} +time.condition.sunny=Soleado +time.condition.daylight=De dia +time.condition.dark=Oscuro +time.condition.windy=Ventoso +time.condition.raining=Lloviendo +time.condition.storming=Tormenta +time.condition.moon_and_stars=Luna y estrellas +time.event.area_expanded=El area se ha expandido! Nuevo territorio descubierto al este. +time.event.mountain_discovered=Se ha descubierto una cordillera al este! +time.event.invasion_source_mountains=las montanas +time.event.invasion_source_new_area=el area nueva +time.event.invasion_start={enemy_plural} estan invadiendo desde {source}! +time.event.zombie_swarm_spotted=Se ha detectado una horda de zombis. +time.event.survivor_joins_one=Un superviviente se une a tu base. +time.event.survivor_joins_many={count} supervivientes se unen a tu base. +time.event.base_max_capacity=Tu base esta en capacidad maxima. +time.event.invasion_ended=La invasion de {enemy} ha terminado. +time.event.blessing_speed_fades=La bendicion de velocidad se desvanece. +time.event.blessing_resident_fades=El proposito de los residentes se desvanece. +time.event.blessing_search_fades=La vista del aguila se desvanece. +time.event.sun_setting=El sol se esta poniendo. +time.event.sky_brightening=El cielo empieza a aclararse. +time.event.residents_repaired_barricade=Los residentes repararon la barricada. +{health} de salud. +time.event.incense_burned_out=El incienso se ha consumido. +combat.burning=Ardiendo! Quedan {health} de salud. +combat.health_status={health} de salud. +combat.you_died=Has muerto. +enemies.residents_killed_attacker=Los residentes mataron a un {enemy} atacante. +enemies.resident_taken=Un residente ha sido llevado. +action.fire.intensity.giant_blaze=Gran llamarada +action.fire.intensity.small_fire=Fuego pequeno +action.fire.intensity.glowing_coals=Unas pocas brasas +action.fire.estimate.less_than_one_hour=menos de 1 hora +action.fire.estimate.approximately_one_hour=aproximadamente 1 hora +action.fire.estimate.approximately_hours=aproximadamente {hours} horas +action.fire.status={label}. Quedan {estimate}. +action.no_fire_nearby=No hay fuego cerca. +action.no_snare_nearby=No hay trampa cerca. +action.snare_holds=La trampa tiene {catch_type}. +action.snare_set_inactive=La trampa esta colocada pero aun no activa. +action.snare_set_empty=La trampa esta colocada y vacia. +action.fish_on_line=Hay un pez en la linea. +action.no_fish_on_line=No hay pez en la linea. +action.cannot_place_snares_in_base=No puedes colocar trampas en la base. +action.snare_already_here=Ya hay una trampa aqui. +action.snare_set=Trampa colocada. +action.no_snares_to_place=No tienes trampas para colocar. +action.feed_fire.stick=Arrojas un brazo de palos al fuego. +action.feed_fire.vine=Arrojas algunas lianas y hojas al fuego. +action.feed_fire.log=Arrojas un tronco al fuego. +action.no_altar_built=No hay altar construido. +action.need_clay_pot_for_incense=Necesitas una olla de arcilla para quemar incienso. +action.no_incense_to_burn=No hay incienso para quemar. +action.incense_burning=Incienso encendido. Quedan {hours} horas. +action.no_sticks_to_feed_fire=No hay palos para alimentar el fuego. +action.dumped_sticks=Arrojaste {amount} palos al fuego. +action.no_vines_to_feed_fire=No hay lianas para alimentar el fuego. +action.dumped_vines=Arrojaste {amount} lianas al fuego. +action.no_logs_to_feed_fire=No hay troncos para alimentar el fuego. +action.dumped_logs=Arrojaste {amount} troncos al fuego. +action.burned_incense=Quemaste {amount} incienso. +{hours} horas. +action.option.place_snare=Colocar trampa +action.option.feed_fire_stick=Alimentar fuego con palo +action.option.feed_fire_vine=Alimentar fuego con liana +action.option.feed_fire_log=Alimentar fuego con tronco +action.option.burn_incense=Quemar incienso +action.option.check_fire=Revisar fuego +action.option.check_snare=Revisar trampa +action.option.check_fishing_pole=Revisar cana de pescar +action.menu.prompt=Menu de accion. {option} +action.cant_do_that=No puedes hacer eso. +base_info.not_in_base=No estas en la base. +base_info.menu.no_options=Info de base. Sin opciones. +base_info.menu.prompt=Info de base. {option} +base.barricade_fallen=La barricada ha caido! +base.no_food_residents_hungry=No hay comida, los residentes tienen hambre. +base.resident_fishing_pole_broke_one=La cana de pescar de un residente se rompio. +base.resident_fishing_pole_broke_many=Se rompieron {count} canas de pescar. +base.resident_caught_fish_one=Un residente atrapo un pescado y lo agrego al almacen. +base.resident_caught_fish_many=Los residentes atraparon {count} pescados y los agregaron al almacen. +base.resident_smoked_fish=Un residente ahumo un pescado y obtuvo {yield} pescado ahumado. +base.livestock_produced=El ganado produjo {meat} carne, {skins} pieles y {feathers} plumas, y se agrego al almacen. +base.resident_spear_broke_one=La lanza de un residente se rompio por desgaste. +base.resident_spear_broke_many=Se rompieron {count} lanzas por desgaste. +base.resident_sling_broke_one=La honda de un residente se rompio por desgaste. +base.resident_sling_broke_many=Se rompieron {count} hondas por desgaste. +base.resident_bow_broke_one=El arco de un residente se rompio por desgaste. +base.resident_bow_broke_many=Se rompieron {count} arcos por desgaste. +base.resident_clothing_repair_one=Un residente esta remendando ropa. Uso {vines} lianas, {skins} pieles y {down} plumon. +base.resident_clothing_repair_many=Los residentes estan remendando ropa. Usaron {vines} lianas, {skins} pieles y {down} plumon. +base.snare_escape_during_check=Un {catch_type} escapo mientras un residente revisaba la trampa en x {position}. +base.resident_retrieved_from_snare=Un residente recupero {game_type} de la trampa en x {position} y 0 y la reinicio. +base.resident_knife_broke_butchering=El cuchillo de un residente se rompio mientras descuartizaba. +base.resident_butchered_result=Un residente descuartizo {game_type}. Agrego {meat} carne, {skins} pieles, {feathers} plumas, {down} plumon y {sinew} tendones al almacen. +base.resident_added_to_storage=Un residente agrego {item} al almacen. +base.resident_basket_broke_one=La cesta de un residente se rompio. +base.resident_basket_broke_many=Se rompieron {count} cestas. +base.resident_basket_broke_foraging_one=La cesta de un residente se rompio mientras recolectaba. +base.resident_basket_broke_foraging_many=Se rompieron {count} cestas mientras recolectaban. +base.resident_gathered_basket_food_one=Un residente reunio una cesta de frutas y nueces. +base.resident_gathered_basket_food_many=Los residentes reunieron {count} cestas de frutas y nueces. +fishing.size.small=pequeno +fishing.size.medium=mediano +fishing.size.large=grande +fishing.size.monster=monstruoso +fishing.pole_broke.default=Tu cana de pescar se rompio. +fishing.pole_broke.switched_weapons=Cambiaste de arma y tu cana de pescar se rompio. +fishing.pole_broke.moved_fish_got_away=Te moviste y el pez escapo. Tu cana de pescar se rompio. +fishing.pole_broke.moved=Te moviste y tu cana de pescar se rompio. +fishing.require_pole_equipped=Necesitas tener equipada una cana de pescar. +fishing.require_near_stream=Necesitas estar a 2 casillas o menos de un arroyo. +fishing.fish_on_line=Hay un pez en el anzuelo! +fishing.caught=Atrapaste un {fish} {size}. +fishing.fish_slipped_off_water=El pez se solto en el agua. +fishing.fish_got_away=El pez se escapo. +fishing.fish_still_on_line=El pez sigue en el anzuelo. +fishing.stopped=Dejas de pescar. +altar.must_be_in_base=Debes estar en la base para usar el altar. +altar.no_altar_built=No hay altar construido. +altar.runed_cannot_sacrifice=Los objetos runicos no se pueden sacrificar. +altar.nothing_to_sacrifice=Nada que sacrificar. +altar.sacrificed_one=Sacrificaste 1 {item}. Favor +{gain}. Total {total}. +altar.sacrificed_many=Sacrificaste {count} {items}. Favor +{gain}. Total {total}. +altar.menu.prompt=Altar. Favor {favor}. +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. +storage.runed_item_name={equipment} runico de {rune} +storage.deposited_one=Depositaste {name}. +storage.deposited_many=Depositaste {amount} {item}. +storage.withdrew_one=Retiraste {name}. +storage.withdrew_many=Retiraste {amount} {item}. +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. +character.info.name_sex=Nombre {name}, {sex} +character.info.name_unknown=Nombre desconocido +character.info.health=Salud {health} de {max} +character.info.weapon=Arma {weapon} +character.info.clothing_equipped=Ropa equipada: {items} +character.info.no_clothing=No llevas ropa equipada. +character.info.missing=Falta: {slots} +character.info.favor=Favor {favor} +character.info.speed=Velocidad {status} +character.info.pet=Mascota {pet}, salud {health} de {max_health}, lealtad {loyalty} de {max_loyalty} +character.info.pet_knocked_out=Inconsciente por {hours} {hour_label}. +character.info.pet_none=No tienes mascota. +character.word.hour=hora +character.word.hours=horas +pet.default_name=mascota +pet.returned=Un {pet} regresa contigo. +pet.knocked_out=Tu {pet} ha quedado inconsciente. +pet.knocked_out_with_remaining=Tu {pet} ha quedado inconsciente. Quedan {hours} {hour_label}. +pet.leaves={pet} se va. +pet.leaving=Tu {pet} se esta yendo. +pet.on_the_way=Tu mascota esta en camino. +pet.hungry_unresponsive=Un {pet} tiene hambre y no responde. +pet.joins_you=Un {pet} se une a ti. +pet.offer.prompt=Un {pet} de aspecto amigable te pide comida. Aceptas? +pet.offer.declined=Rechazado. +pet.retrieve.cant_carry_chew_on=No puedes llevar {item}, asi que se lo das a un {pet} para que lo mastique. +pet.retrieve.retrieved_item=Tu {pet} recupero {item}. +pet.retrieve.random_find=Tu {pet} recupero {count} {item}. +pet.recovered_from_injuries=Tu {pet} se recupero de sus heridas. +pet.getting_hungry=Un {pet} se esta poniendo hambriento. +fylgja.already_used_today=Ya usaste tu Fylgja hoy. +fylgja.menu.prompt=Menu de Fylgja. +fylgja.need_open_ground_to_charge=Necesitas terreno abierto frente a ti para cargar. +fylgja.option=Fylgja de {name} +fylgja.connection_with_target=Tienes una conexion {stage} con {target}. +fylgja.unlocked=Has desbloqueado el Fylgja de {name}! +fylgja.already_unlocked=Ya has desbloqueado el Fylgja de {name}. +fylgja.name.unicorn=Unicornio +fylgja.target.unicorn=unicornio +fylgja.stage.tenuous=tenue +fylgja.stage.faint=debil +fylgja.stage.stirring=creciente +fylgja.stage.budding=incipiente +fylgja.stage.kindled=encendida +fylgja.stage.bound=vinculada +fylgja.stage.sworn=jurada +fylgja.stage.ascendant=ascendente +fylgja.stage.ultimate=suprema +quest.name.bat_invasion=Invasion de murcielagos +quest.name.catch_boomerang=Atrapa el boomerang +quest.name.enchanted_melody=Melodia encantada +quest.name.escape_from_hel=Escapa de Hel +quest.name.skeletal_bard=Bardo esqueletico +quest.name.unknown=Mision desconocida +quest.description.bat_invasion=Invasion de murcielagos. Murcielagos asesinos gigantes te atacan. Pulsa espacio para lanzar cuando el murcielago suene centrado. +quest.description.catch_boomerang=Atrapa el boomerang. Lanza y atrapa el boomerang al volver para ganar hasta 4 puntos. +quest.description.enchanted_melody=Melodia encantada. Repite el patron con E R D F o U I J K. De tono mas bajo a mas alto. +quest.description.escape_from_hel=Escapa de Hel. Pulsa espacio para saltar sobre tumbas abiertas. El ritmo se acelera. +quest.description.skeletal_bard=Bardo esqueletico. Un esqueleto llamado Billy Bones practica para ser bardo. Cuenta las notas. +quest.description.unknown=Mision desconocida. +quest.new_available=Hay una nueva mision disponible: {quest}. +quest.none_available=No hay misiones disponibles. +quest.favor_phrase.displeased=Los dioses estan disgustados con tu desempeno. +quest.favor_phrase.pleased=Los dioses estan complacidos con tu desempeno. +quest.favor_phrase.very_pleased=Los dioses estan muy complacidos con tu desempeno. +quest.favor_phrase.extremely_pleased=Los dioses estan extremadamente complacidos con tu desempeno. +quest.favor_phrase.ultimately_pleased=Los dioses estan totalmente complacidos con tu desempeno. +quest.rewards.title=Recompensas de mision +quest.rewards.complete_heading=Mision completada! +quest.rewards.heading=Recompensas: +quest.rewards.favor_line=Favor: +{favor} +quest.rewards.item_line={item}: +{amount} +quest.rewards.inventory_full_line=Inventario lleno, no pudiste recibir {item}. +quest.rewards.no_items_awarded=No se otorgaron objetos. +quest.rewards.score_line=Puntuacion: {score} +quest.menu.prompt=Menu de misiones. +quest.menu.count={index} de {total} +quest.minigame.starting=Comenzando. +quest.bat_invasion.complete_score=Invasion de murcielagos completada. Puntuacion {score}. +quest.boomerang.press_space_to_throw=Pulsa Espacio para lanzar. +quest.boomerang.skill.almost_dropped=casi lo dejas caer +quest.boomerang.skill.managed_to_catch=lograste atraparlo +quest.boomerang.skill.caught=lo atrapaste +quest.boomerang.skill.expertly_caught=lo atrapaste con maestria +quest.boomerang.caught_result=Tu {skill} el boomerang. {points} puntos.{suffix} +quest.boomerang.missed=Fallaste el boomerang.{suffix} +quest.boomerang.hit_you=El boomerang te golpeo.{suffix} +quest.boomerang.summary=Atrapaste {count} boomerangs para un total de {score} puntos. +quest.enchanted_melody.practice_prompt=Modo practica. Pulsa Enter para comenzar, Escape para cancelar. +quest.enchanted_melody.starting_prompt=Comenzando. Repite el patron. +quest.enchanted_melody.score=Acertaste {rounds} notas. Puntuacion {score}. +quest.skeletal_bard.select_notes=Selecciona el numero de notas. {selection}. +quest.skeletal_bard.round_result=Ingresaste {guess} notas y el numero real era {actual}. {points} puntos. Pulsa Enter para continuar. +quest.skeletal_bard.complete_score=Bardo esqueletico completado. Tu puntuacion de las cinco melodias es {score}. +quest.escape_from_hel.fell_in_score=Te caíste. Puntuacion {score}. +adventure.none_in_base=No hay aventuras disponibles en la base. +adventure.already_attempted_today=Ya intentaste una aventura hoy. +adventure.option.unicorn_hunt=Caza de unicornio (Jefe de montana) +adventure.option.bandit_hideout=Escondite de bandidos +adventure.none_in_area=No se encontraron aventuras en esta zona. +adventure.menu.prompt=Menu de aventuras. +adventure.victory=Victoria! +adventure.rewards.title==== Recompensas de victoria === +adventure.unicorn.flee=Huiste del unicornio. +adventure.unicorn.position_report=X {x}, {terrain}, mirando {direction} +adventure.unicorn.player_trampled=El unicornio te atropello! +adventure.unicorn.victory_title=Victoria del Unicornio +adventure.unicorn.reward.favor_awarded=Los dioses estan complacidos con tu victoria! Se otorgo {favor} de favor. +adventure.unicorn.reward.heal_scrolls=Pergaminos de curacion: +{count}. +adventure.unicorn.reward.learned_rune_swiftness=Aprendiste la Runa de Celeridad! +adventure.unicorn.reward.engrave_unlocked=Ahora puedes grabar equipo con esta runa en el menu de fabricacion. +adventure.unicorn.reward.mastered_rune_swiftness=Ya dominaste la Runa de Celeridad. +adventure.unicorn.horses.no_stable_or_storage=No hay establo o almacen construido. No se capturaron caballos. +adventure.unicorn.horses.stable_full=El establo esta lleno. No se capturaron caballos. +adventure.unicorn.horses.joined_stable=Un caballo se une a tu establo. +adventure.unicorn.horses.none_captured=No se capturaron caballos. +adventure.bandit_hideout.flee=Huiste del escondite de bandidos. +adventure.bandit_hideout.position_report=X {x}, {terrain}. Distancia a la base {distance}. Mirando {direction}. Salud de la base {health} de {max}. +adventure.bandit_hideout.player_defeated=Fuiste derrotado en el escondite de bandidos. +character.report.position={direction}, x {x}, y {y}, terreno {terrain} +character.report.terrain_mountains={terrain}. Montanas +character.report.player={health} de {max} de salud +character.report.pet={pet}: {health} de {max} de salud, lealtad {loyalty} de {max_loyalty} +item.label.items=objetos +item.label.item=objeto +item.label.unknown=Desconocido +equipment.menu.prompt=Menu de equipo. {option} +equipment.menu.nothing_to_equip=Nada para equipar. +equipment.menu.equipped_suffix=(equipado) +equipment.set_to_slot={name} asignado a la ranura {slot}. +equipment.equipped={name} equipado. +equipment.unequipped={name} desequipado. +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. +environment.fall.damage_report=Caíste {height} pies! Recibiste {damage} de dano. Quedan {health} de salud. +environment.snare.collected_with_catch=Recogiste {catch_type} y trampa. +environment.snare.collected_empty=Recogiste trampa. +environment.tree.cut_down=Este arbol ha sido talado. +environment.tree.empty=Este arbol esta vacio. +environment.area.nothing_left=No queda nada en esta zona. +environment.tree.climb_started=Empezaste a subir al arbol. Altura: {height} pies. +environment.tree.climb_reached_top=Llegaste a la cima a {height} pies. +environment.tree.climb_reached_ground=Llegaste al suelo sin problemas. +environment.tree.climb_down=Bajando. +environment.movement.need_rope=Necesitaras una cuerda para subir ahi. +environment.movement.press_up_climb=Pulsa arriba para subir. +environment.movement.press_down_climb=Pulsa abajo para bajar. +environment.movement.need_canoe=Necesitas una canoa para cruzar agua profunda. +environment.rope_climb.direction.up=arriba +environment.rope_climb.direction.down=abajo +environment.rope_climb.progress=Subiendo {direction}. {distance} pies. +environment.rope_climb.reached_elevation=Alcanzaste la elevacion {elevation}. +snare.escape_at=Un {catch_type} escapo de tu trampa en x {position} y 0! +snare.caught_at={catch_type} atrapado en trampa en x {position} y 0! +snare.stepped_on_with_escape=Pisaste tu trampa! El {catch_type} escapo. +snare.stepped_on_broke=Pisaste tu trampa y la rompiste! +fire.low_fuel_at=El fuego en x {position} y {y} se esta quedando sin combustible! +fire.went_out_at=El fuego en x {position} y {y} se apago. +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.in_progress=Fabricando... +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.weapons.crafted.spear=Fabricaste una lanza. +crafting.weapons.crafted.sling=Fabricaste una honda. +crafting.weapons.crafted.bow=Fabricaste un arco. +crafting.weapons.crafted.stone_axe=Fabricaste un hacha de piedra. +crafting.weapons.crafted_max.spears=Fabricaste {count} lanzas. +crafting.weapons.crafted_max.slings=Fabricaste {count} hondas. +crafting.weapons.crafted_max.bows=Fabricaste {count} arcos. +crafting.weapons.crafted_max.stone_axes=Fabricaste {count} hachas de piedra. +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.tools.crafted.stone_knife=Fabricaste un cuchillo de piedra. +crafting.tools.crafted.snare=Fabricaste una trampa. +crafting.tools.crafted.fishing_pole=Fabricaste una cana de pescar. +crafting.tools.crafted.rope=Fabricaste cuerda. +crafting.tools.crafted.quiver=Fabricaste un carcaj. +crafting.tools.crafted.canoe=Fabricaste una canoa. +crafting.tools.crafted.reed_basket=Fabricaste una cesta de junco. +crafting.tools.crafted.clay_pot=Fabricaste una olla de arcilla. +crafting.tools.crafted_max.stone_knives=Fabricaste {count} cuchillos de piedra. +crafting.tools.crafted_max.snares=Fabricaste {count} trampas. +crafting.tools.crafted_max.fishing_poles=Fabricaste {count} canas de pescar. +crafting.tools.crafted_max.ropes=Fabricaste {count} cuerdas. +crafting.tools.crafted_max.quivers=Fabricaste {count} carcajes. +crafting.tools.crafted_max.canoes=Fabricaste {count} canoas. +crafting.tools.crafted_max.reed_baskets=Fabricaste {count} cestas de junco. +crafting.tools.crafted_max.clay_pots=Fabricaste {count} ollas de 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.materials.not_enough_quiver_capacity_for_arrows=No hay suficiente capacidad en el carcaj para flechas. +crafting.materials.crafted.arrows=Fabricaste {count} flechas. +crafting.materials.crafted.bowstring=Fabricaste una cuerda de arco. +crafting.materials.crafted_max.bowstrings=Fabricaste {count} cuerdas de arco. +crafting.materials.crafted.incense=Fabricaste incienso. +crafting.materials.crafted_max.incense=Fabricaste {count} incienso. +crafting.materials.smoke_fish.crafted=Ahumaste un pescado y obtuviste {yield} pescado ahumado. +crafting.materials.smoke_fish.crafted_max=Ahumaste {count} pescados y obtuviste {yield} pescado ahumado. +crafting.materials.butcher.goose=Descuartizaste un ganso. Obtuviste 1 carne, plumas y plumon. +crafting.materials.butcher.turkey=Descuartizaste un pavo. Obtuviste 1 carne y plumas. +crafting.materials.butcher.boar=Descuartizaste un jabali. Obtuviste carne, 3 pieles y 2 tendones. +crafting.materials.butcher.default=Descuartizaste {game}. Obtuviste 1 carne y 1 piel. +crafting.materials.butcher.no_space_outputs=No hay espacio para los resultados. +crafting.materials.butcher.max_result=Descuartizaste {count} caza. Obtuviste {meat} carne, {skins} pieles, {feathers} plumas, {down} plumon y {sinew} tendones. +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.clothing.crafted.skin_hat=Fabricaste un sombrero de piel. +crafting.clothing.crafted.skin_gloves=Fabricaste guantes de piel. +crafting.clothing.crafted.skin_pants=Fabricaste pantalones de piel. +crafting.clothing.crafted.skin_tunic=Fabricaste una tunica de piel. +crafting.clothing.crafted.moccasins=Fabricaste mocasines. +crafting.clothing.crafted.skin_pouch=Fabricaste una bolsa de piel. +crafting.clothing.crafted.backpack=Fabricaste una mochila. +crafting.clothing.crafted_max.skin_hats=Fabricaste {count} sombreros de piel. +crafting.clothing.crafted_max.skin_gloves=Fabricaste {count} guantes de piel. +crafting.clothing.crafted_max.skin_pants=Fabricaste {count} pantalones de piel. +crafting.clothing.crafted_max.skin_tunics=Fabricaste {count} tunicas de piel. +crafting.clothing.crafted_max.moccasins=Fabricaste {count} mocasines. +crafting.clothing.crafted_max.skin_pouches=Fabricaste {count} bolsas de piel. +crafting.clothing.crafted_max.backpacks=Fabricaste {count} mochilas. +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.buildings.none_available=No hay edificios disponibles. +crafting.buildings.firepit.already_in_base=Ya hay una fogata en la base. +crafting.buildings.firepit.already_here=Ya hay una fogata aqui. +crafting.buildings.firepit.built=Fogata construida aqui. +crafting.buildings.fire.requires_firepit_nearby=Necesitas una fogata a menos de 2 casillas para construir un fuego. +crafting.buildings.fire.already_in_base=Ya hay un fuego en la base. +crafting.buildings.fire.built=Fuego construido en la fogata. +crafting.buildings.herb_garden.base_only=El huerto de hierbas solo puede construirse en la base. +crafting.buildings.herb_garden.already_in_base=Ya hay un huerto de hierbas en la base. +crafting.buildings.herb_garden.built=Huerto de hierbas construido. La base ahora cura mas rapido. +crafting.buildings.storage.base_only=El almacen debe construirse en la base. +crafting.buildings.storage.fully_upgraded=El almacen ya esta completamente mejorado. +crafting.buildings.storage.upgraded=Almacen mejorado. La capacidad ahora es {capacity} por objeto. +crafting.buildings.pasture.base_only=El corral debe construirse en la base. +crafting.buildings.pasture.requires_storage_upgrade=Debes mejorar el almacen antes de construir un corral. +crafting.buildings.pasture.already_built=El corral ya esta construido. +crafting.buildings.pasture.built=Corral construido. +crafting.buildings.stable.base_only=El establo debe construirse en la base. +crafting.buildings.stable.requires_storage_upgrade=Debes mejorar el almacen antes de construir un establo. +crafting.buildings.stable.already_built=El establo ya esta construido. +crafting.buildings.stable.built=Establo construido. +crafting.buildings.altar.base_only=El altar debe construirse en la base. +crafting.buildings.altar.already_built=El altar ya esta construido. +crafting.buildings.altar.built=Altar construido. +crafting.runes.base_only=El grabado de runas solo puede hacerse en la base. +crafting.runes.option={rune} (1 Arcilla, 1 Favor) [Requiere Cuchillo] +crafting.runes.none_unlocked=Aun no hay runas desbloqueadas. +crafting.runes.prompt=Runas. {option} +crafting.runes.equipment_option={name} ({count} disponibles) +crafting.runes.no_equipment_available=No hay equipo disponible para grabar. +crafting.runes.select_equipment_prompt=Selecciona equipo para grabar con {rune}. {option} +crafting.runes.no_item_available=No hay {item} disponible. +crafting.runes.runed_name={equipment} rúnico de {rune} +crafting.runes.engraved=Grabaste {item}. +crafting.runes.engraved_max=Grabaste {count} {item} con {rune}. +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.barricade.full_health=La barricada ya tiene salud completa. +crafting.barricade.no_materials=No hay materiales para reforzar la barricada. +crafting.barricade.reinforced.sticks=Reforzaste la barricada con palos. +{gained} de salud. Ahora {health} de {max}. +crafting.barricade.reinforced.vines=Reforzaste la barricada con lianas. +{gained} de salud. Ahora {health} de {max}. +crafting.barricade.reinforced.log=Reforzaste la barricada con tronco. +{gained} de salud. Ahora {health} de {max}. +crafting.barricade.reinforced.stones=Reforzaste la barricada con piedras. +{gained} de salud. Ahora {health} de {max}. +crafting.barricade.reinforced_max.sticks=Reforzaste la barricada {count} veces con palos. Salud ahora {health}. +crafting.barricade.reinforced_max.vines=Reforzaste la barricada {count} veces con lianas. Salud ahora {health}. +crafting.barricade.reinforced_max.log=Reforzaste la barricada {count} veces con tronco. Salud ahora {health}. +crafting.barricade.reinforced_max.stones=Reforzaste la barricada {count} veces con piedras. Salud ahora {health}. +crafting.barricade.not_enough.sticks=No tienes suficientes palos. +crafting.barricade.not_enough.vines=No tienes suficientes lianas. +crafting.barricade.not_enough.logs=No tienes suficientes troncos. +crafting.barricade.not_enough.stones=No tienes suficientes piedras. +crafting.missing=Falta: {requirements} +crafting.requirement.stone_knife=Cuchillo de piedra +crafting.requirement.game=Caza +crafting.requirement.favor=favor +crafting.requirement.resources=recursos + +[messages] +msg.00884428322b=foso +msg.01a159425acf=romper trampa +msg.03268c7d7239=mochilas +msg.03976920927e=cuerda +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.066723a92e94==== Victoria Recompensas === +msg.07e9ab246cdd=Despues de cerrar esta pantalla, puedes practicar las notas. +msg.08b42842d5bc=tendon +msg.096539f23463=Retumban los tambores de guerra! Preparate para el combate! +msg.09c28ae8f83b=Piedras +msg.09d016686864=Establo no construido +msg.0a3bdcef1906=Cestas de junco +msg.0a9d6438c818==== Melodia Encantada === +msg.0b3d8d5cdca6=- Llega a la base y destruye la barricada +msg.0bcd9af79f2d=bosque +msg.0c7789cf9a52=fabricacion completa +msg.0cf604cb001b=Flechas +msg.0d1ec2e04286=honda golpe +msg.0e16a11cda9a=cuchillos +msg.0e57fb642209=lanza +msg.0eb796e65c39=Hondas +msg.0f9af12cbb82=piel bolsa +msg.126dd3b70a5c=Troncos +msg.136e4cb03bd5=No se pudo guardar el archivo +msg.145ba8a7a0f2=carcajes +msg.14f5f5ff4f76=No se encontraron supervivientes. +msg.16134a4250f7=caza cae +msg.16a233f44489=- Escucha las tumbas abiertas acercandose (cada vez mas fuerte) +msg.176b9a0c8398=Seguro que quieres salir? +msg.17808e3775e1=incienso palo +msg.17b5b2a286cc==== Bardo Esqueletico === +msg.18b837bb6f43=murcielago1 +msg.18c77e824a2a=yeti muere +msg.1a853cbfbc36=bandido muere +msg.1bb8ef2886a3=No muerto basico. +msg.1be2a44cb53d=noche +msg.1bfa6bc809d4==== Escondite de Bandidos === +msg.1c7022965d1a=piel guante +msg.1d15d0d5c98d=Almacen construido. Total objetos {arg1} +msg.1d61343b9660=Almacen no construido. Sin recompensas de objetos. +msg.1da840244989=fuego +msg.1db76ac85135=honda +msg.237b5017397b=hacha +msg.245c89590526=- Cuando el puente colapse con el Unicornio encima, ganas! +msg.25db7a4ec692=Se recupero ganado y se agrego a tu corral. +msg.2630bd23d7d4=seguro para salto +msg.2648db667f58=lanza golpe +msg.270dcc07d3ae=trueno medio +msg.271e9a568c0e=salto +msg.2727056eb878=Corral construida. Ganado {arg1} de {arg2} +msg.273d131f0c8e=Sin notificaciones. +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.29f291640516=Instrucciones de mision +msg.2afb87ae1752=- Corres automaticamente, la velocidad aumenta con el tiempo +msg.2bb11d7c39dc=Un enemigo esta al alcance de tu arma equipada actualmente. +msg.2bf7b1d59f34=unicornio cae +msg.2c6c80931cd1=Piel Sombreros +msg.2e094dd0a2ab=lanza golpe +msg.3030e8ec7633=Pescado +msg.33bc40d8201e=- Despues de la melodia, elige la cantidad de notas +msg.33c89b4bdc4e=carcaj +msg.34daac1528e0=menu mover +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.37423f3bd00f=pieles +msg.37ef0562902c=liana +msg.381aea2617cb=Incienso +msg.38a7045d078c=trueno bajo +msg.38eb0fe3f78a=flecha flies +msg.39784ab5ac68=piece de tendon +msg.39a531d92bbe=flecha +msg.3a7d9767b123=objeto +msg.3c32451c9c71=piel bolsas +msg.3cebb4c50866=cana de pescar +msg.3e024b87e650=duro piedra +msg.3e03229d39e4=Pieles +msg.3eb9cd52ec8d=piel guantes +msg.3f638a51cb47=E o U - Cuarta nota (tono mas alto) +msg.4010ceb056dd=trueno alto +msg.403654dac805=hueso4 +msg.4037ea7ad431=lanzas +msg.408a612a4167=espectro1 +msg.412fed81f58a=junco basket +msg.417705583821=viento medio +msg.41a682cadfe3=volar +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.437ecef821db=lanza falla +msg.43821cae64c5=cuerdas de arco +msg.444d4678962b=caida +msg.4537c16badd9=arco fuego +msg.455b11a16eb7=trampas +msg.46fcdd91c99e=paso draugrhaugr +msg.474c797713f3=troncos +msg.483a17dd2a17=Trampas +msg.494f87e3874d=draugrhaugr 1 +msg.495f723e9fa1=hachas +msg.4a0e932d1bc5=grava +msg.4a17d4fabe93=piel tunicas +msg.4b9b4385b1a9=bumeran +msg.4c0d2469f78b=piedras +msg.4f08301dcb7b=notificacion +msg.4f11c6797b8d=poco profunda agua +msg.4f773e9d5bf0=Arcos +msg.4f7f358cd341=ganso +msg.4f99d3d5b638=Arcilla Ollas +msg.5009c8e190ee=pavo +msg.508e8bc0cc57=- Usa Arriba/Abajo para cambiar el numero +msg.5123c22ce6a5=honda bajo alcance +msg.528fb1c4e5fb=Nuevo juego +msg.5460f2e3c43e=aplastar +msg.546330ee64db=cana de pescar romper +msg.54fc2d657ed6=Draugnorak +msg.55748cf43121=vampiro3 +msg.5665ef30ef4d=Los murcielagos invaden! Lanza tu lanza para defenderte. +msg.584227d57290=Juncos +msg.5906872f9bb6=vampiro2 +msg.596350ae8387=Encontraste un junco. +msg.599096c44c5b=Jabali Canales +msg.5993f9da16fc=Aprender sonidos. +msg.5b61e4982b59=espectro muere +msg.5bdb7fa2223d=Plumas +msg.5c2434ecd90f=juncos +msg.5cac31311668=El corral esta lleno. No se recupero ganado. +msg.5cc7429297c8=ese puedes ser destroyed con un hacha. +msg.5dfc869b7588=lanzamiento fuerza +msg.5ea84b1201bb={arg1} favor grants tu el eyes de un aguila. +msg.5f3f3a85393b=menu seleccionar +msg.623f78278373=Barricada salud {arg1} de {arg2} +msg.62583aa87a59=Corral o almacen no construido. No se recupero ganado. +msg.627a5b9e6322=trepar arbol +msg.62fac2f438bb=halcon +msg.64875fcccaac=pescado +msg.64b945d9072e=Huerto de hierbas no construido +msg.65eb0fb6ab40=piel pantalones +msg.66fd9a7e91f8=lianas +msg.67ae077e6458=Huerto de hierbas construido +msg.686021c25738=hueso3 +msg.6861fd95f4f7=- Hay 5 turnos +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.704e634dd69f=golpe suelo +msg.714165bec103=colocar trampa +msg.717d7fcc68b7=- Hay 5 rondas, hasta 4 puntos cada una +msg.71aad5f523eb=piel sombreros +msg.731152a028c6=Cerrado. +msg.7680f62917b1=- Presiona Space otra vez cuando suene cerca +msg.77346d0447da=plumon +msg.7748b68d141a=profundo bosque +msg.78f036ea9ebd=El beast charges back y forth relentlessly. +msg.7921110eb7e1=fabricacion +msg.7968afd0c044=pesca cañas +msg.7babc233de26=tronco +msg.7c49f713a0fb=flechas +msg.7c57f16628c7=Encontraste arcilla. +msg.7d2cf35450e3=Escucha al melody y cantidad el notas. +msg.7d88f361f3e8=zombi1 +msg.7e83de91b4a2=bandido hembra muere +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.81df04a81e24={arg1} favor shines upon tu. {arg2} +msg.823be948275f=- Un mejor tiempo da mas puntos (hasta 4) +msg.83865f248682=No muertos mas poderosos. No te pierden el rastro cuando estas en la base. +msg.863be329d064=dialogo +msg.8443de859917=viento alto +msg.849a4f4a399d=Tu son huyendo desde el realm del muertos! +msg.85a177a79f1d=hueso2 +msg.85f15bc57d6c=pluma +msg.862021235188=canoa +msg.86290fe7e711=hueso5 +msg.86e257c3efcb=Notificacion mas antigua. {arg1} +msg.87383ce4344d=Cuerdas de arco +msg.87cdb5c91437=cuerdas +msg.8818f1d55166={arg1} de radiancia llena de proposito a los residentes. +msg.88749dbbbf09==== Murcielago Invasion === +msg.88a8bee5206e=- Atrae al Unicornio al puente +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.917ee46db0cd=abeja +msg.924e0eb6b6f1=jugador masculino danio +msg.9281d2fe5f08={arg1} favor shines upon tu. Tu feel swift para un mientras. +msg.92abc6d6953f=trepar cuerda +msg.9379874d592b=Altar no construido. No favor otorgado. +msg.93894d088f6a=- Usa tu hacha para destruir un soporte del puente +msg.946466f6c963=palo +msg.957f554c342a=jabalI +msg.97c59037a63c=Un superviviente se une a tu base. +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.9bbcfc6c8f9d=bandido4 +msg.9c19f78d931d=hueso6 +msg.9dd40321998e=murcielago2 +msg.a040b931b9bb=Como a jugar: +msg.a153dba5e67a=profundo agua +msg.a2620cbc10f5=dia +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.a6caf0c3771d=hondas +msg.a6e2492f34b6==== Escape desde Hel === +msg.a79dc18e77d9=- Tienes 10 intentos +msg.a7cb7754af6c=Notificacion mas reciente. {arg1} +msg.a95eab2b45da=- Salta (flecha ARRIBA) para evitar ser atropellado +msg.aa3a1916166d=duende1 +msg.aa5edb7494f3=arcilla olla +msg.ab2244bc6027=pergamino de curacion +msg.abe73973cc7c=No se pudo cargar la partida. +msg.ac36af678d78=Controles: +msg.ad34f00c46cd=mocasin +msg.ad36d64ad3a3=duende muere +msg.af93482d73f0=pergaminos de curacion +msg.aff5100bebc5=negro gato +msg.b0cca9f9205a=Palos +msg.b0f6aa37f763=Lianas +msg.b11aa393c8a8=Mocasines +msg.b31837f2fc5b=iniciar pesca +msg.b32fc272dc0d=- Escucha murcielagos pasar de izquierda a derecha +msg.b48d0661f0da=Piel Tunicas +msg.b5d5cc084a95=bandido3 +msg.b73a0709297f=llamar mascota +msg.b774102c89b5=Cuchillos +msg.b94e9f3d7e00=nieve +msg.ba3af68702a3=Learned Rune de Destruction! +msg.ba7d7dfe330b=Cestas de frutas y nueces +msg.bb0ea68db3b4=cesta de frutas y nueces +msg.bb40f75a9c60=carne +msg.bbc29b76c59b=hierba +msg.bdebda25dad9=flecha golpe +msg.bf93e5ce8bc1=Plumon +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.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.c52888225c69=caida +msg.c65e5dfc5ba7=bandido1 +msg.c72b7844929d=Bandidos defeated: {arg1}. +msg.c82e3d7279ef=arroyo +msg.c86ace9b97f8=Establo construido. Horses {arg1} de {arg2} +msg.c8da1c4f1714=Baja de tono a medida que avanza la caida. +msg.c90399f41b3f=Carne +msg.c9532ab465fb=hueso7 +msg.cb8b9677952a=Almacen recompensas: +msg.cb8e70ab2bcc=jabali canales +msg.cc167fb645e4=cuerda de arco +msg.cc61d0f3de65=cestas de junco +msg.cccc5c842adb=hacha golpe +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.d34b6217ea75=enemigo cae +msg.d373510877fd=Piel Guantes +msg.d39008c4a392=Excepcion no controlada +msg.d49f2e760778=mochila +msg.d537941485fb=R o I - Tercera nota +msg.d609f615ddae=No se encontraron partidas guardadas. +msg.d6dc139f529a=invasion enemiga +msg.d6fd3467a950=yeti 1 +msg.d75f36f88797=Hachas +msg.d7879bd506af=No se recupero ganado. +msg.da0b5a461d98=F o K - Primera nota (tono mas bajo) +msg.da11dc5e090c=honda golpe +msg.da93fb9992a3=vampiro muere +msg.dadd68431e60=Habita regiones nevadas. +msg.db2a8514a0fc=salir alcance +msg.db56e2e1db9f=Aventura +msg.db5bbabf1bc3=bandido2 +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.e00d5664a78e=Arcilla +msg.e0b678c82a60=Un puente de madera cruza un abismo profundo adelante. El puente tiene dos soportes +msg.e142524d7689=Estrategia: +msg.e1c16d0490ee=No hay sonidos disponibles. +msg.e253d297aabc=piel tunica +msg.e2e91797da96=Pesca Cañas +msg.e30bfd0c38dc=piedra +msg.e34161b98188=malo lanzamiento +msg.e3ce3e543239=Carcajes +msg.e3d828480491=honda alto alcance +msg.e4427f09bc09=Cierra esta pantalla para comenzar. +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.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.e9dd1d268b65=La base esta lejos al este, protegida por una barricada. +msg.ea771032e597=Altar construido +msg.ebf229b227f8={arg1} {arg2} de {arg3} +msg.ee200b3d8ae2=Lanzas +msg.eff5a5f34d9a=unicornio en puente +msg.f015f25c409b=arco draw +msg.f12b2d219fe8=Cuerdas +msg.4671b16b0398=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.f305da2c3550==== Atrapa el Bumeran === +msg.f3c5ed579a54=viento bajo +msg.f3d964225e83=jugador hembra danio +msg.f5cabf873590=junco +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.f8fc573e2d2f=Cargar partida (sin partidas guardadas) +msg.fa5bd38090c7=arco +msg.fb4a022a82b3=unicornio galopando +msg.fbec17cb2fcb=lluvia +msg.fca1af3d07b0=Pergaminos de curacion +msg.fd1a25749457=Volumen {arg1}. +msg.fe2a9d6606e0=draugnorak +msg.feea4a3b0a84==== Unicornio Hunt === +msg.fefc084f67de=D o J - Segunda nota +msg.ff5ac0b6df7b=arcos diff --git a/libstorm-nvgt b/libstorm-nvgt index 44f13b1..e999b2f 160000 --- a/libstorm-nvgt +++ b/libstorm-nvgt @@ -1 +1 @@ -Subproject commit 44f13b1aeb9dc9899eff9b30798f8a5068534cb0 +Subproject commit e999b2ff5c964ea044c3eb3e44e30e863d42c5bc 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/scripts/audit_untranslated_strings.py b/scripts/audit_untranslated_strings.py new file mode 100755 index 0000000..356411f --- /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 / "src" / "sound_settings.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/format-nvgt.sh b/scripts/format-nvgt.sh new file mode 100755 index 0000000..4f1c921 --- /dev/null +++ b/scripts/format-nvgt.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Show simple usage help. +print_usage() { + echo "Usage: scripts/format-nvgt.sh [path/to/file.nvgt ...]" + echo "Formats all tracked .nvgt files when no file paths are provided." +} + +scriptDir="" +repoRoot="" +filePath="" +formattedCount=0 +targetFiles=() + +# Help flag is optional and exits early without formatting. +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + print_usage + exit 0 +fi + +# Stop immediately if clang-format is not installed. +if ! command -v clang-format >/dev/null 2>&1; then + echo "clang-format is required but was not found in PATH." >&2 + exit 1 +fi + +# Resolve script location, then run from repo root so relative paths work. +scriptDir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repoRoot="$(cd "${scriptDir}/.." && pwd)" +cd "${repoRoot}" + +# We require the project style file to keep formatting consistent. +if [[ ! -f ".clang-format" ]]; then + echo "Missing .clang-format in repo root: ${repoRoot}" >&2 + exit 1 +fi + +# No args: format every tracked .nvgt file in git. +if [[ "$#" -eq 0 ]]; then + mapfile -t targetFiles < <(git ls-files "*.nvgt") +else + # Args provided: validate each file and format only those paths. + for filePath in "$@"; do + if [[ ! -f "${filePath}" ]]; then + echo "File not found: ${filePath}" >&2 + exit 1 + fi + if [[ "${filePath}" != *.nvgt ]]; then + echo "Only .nvgt files are supported: ${filePath}" >&2 + exit 1 + fi + targetFiles+=("${filePath}") + done +fi + +if [[ "${#targetFiles[@]}" -eq 0 ]]; then + echo "No .nvgt files found to format." + exit 0 +fi + +# Force C++ parsing rules for NVGT while still using repo .clang-format. +for filePath in "${targetFiles[@]}"; do + clang-format -i --style=file --assume-filename=file.cpp "${filePath}" + formattedCount=$((formattedCount + 1)) +done + +echo -n "Formatted ${formattedCount} " +if [[ ${formattedCount} -ne 1 ]]; then + echo "files." +else + echo "file." +fi + +exit 0 diff --git a/scripts/generate_i18n_catalog.py b/scripts/generate_i18n_catalog.py new file mode 100644 index 0000000..a1c8162 --- /dev/null +++ b/scripts/generate_i18n_catalog.py @@ -0,0 +1,1200 @@ +#!/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 / "src" / "sound_settings.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 parse_string_literal_sequence(expr: str) -> Optional[str]: + expr = expr.strip() + if not expr or expr[0] != '"': + return None + + parts: List[str] = [] + i = 0 + length = len(expr) + + while i < length: + while i < length and expr[i].isspace(): + i += 1 + + if i >= length: + break + if expr[i] != '"': + return None + + i += 1 + literal_chars: List[str] = [] + escape = False + + while i < length: + ch = expr[i] + if escape: + literal_chars.append("\\" + ch) + escape = False + i += 1 + continue + + if ch == "\\": + escape = True + i += 1 + continue + + if ch == '"': + i += 1 + break + + literal_chars.append(ch) + i += 1 + else: + return None + + parts.append(unescape_string_literal("".join(literal_chars))) + + if not parts: + return None + + return "".join(parts) + + +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 + + literal_sequence = parse_string_literal_sequence(part) + if literal_sequence is not None: + template_parts.append(literal_sequence) + 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.main_menu.prompt", "Draugnorak. Main menu."), + ("system.game.new_game_started", "New game started."), + ("system.game.loaded", "Game loaded."), + ("system.game.paused_prompt", "Paused. Press backspace to resume."), + ("system.game.resumed", "Resumed."), + ("system.exit.confirm", "Really exit?"), + ("system.direction.west", "west"), + ("system.direction.east", "east"), + ("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.load_game.save_deleted", "Save deleted."), + ("system.load_game.no_saves_found", "No saves found."), + ("system.load_game.unable_to_load", "Unable to load save."), + ("system.load_game.select_prompt", "Load game. Select character. Press delete to remove a 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.menu.main.story", "The story"), + ("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.inventory.max_arrows_with_quivers", "You can only carry {max} arrows with your current quivers."), + ("system.inventory.item_not_available", "Item not available."), + ("system.search.found_item", "Found {item}."), + ("system.search.found_nothing", "Found nothing."), + ("system.pickup.item", "Picked up {item}."), + ("system.time.period.am", "am"), + ("system.time.period.pm", "pm"), + ("system.time.report", "{hour} oclock {period} day {day}"), + ("system.time.report_with_conditions", "{time}. {conditions}"), + ("system.time.condition.sunny", "Sunny"), + ("system.time.condition.daylight", "Daylight"), + ("system.time.condition.dark", "Dark"), + ("system.time.condition.windy", "Windy"), + ("system.time.condition.raining", "Raining"), + ("system.time.condition.storming", "Storming"), + ("system.time.condition.moon_and_stars", "Moon and stars"), + ("system.time.event.area_expanded", "The area has expanded! New territory discovered to the east."), + ("system.time.event.mountain_discovered", "A mountain range has been discovered to the east!"), + ("system.time.event.invasion_source_mountains", "the mountains"), + ("system.time.event.invasion_source_new_area", "the new area"), + ("system.time.event.invasion_start", "{enemy_plural} are invading from {source}!"), + ("system.time.event.zombie_swarm_spotted", "A swarm of zombies has been spotted."), + ("system.time.event.survivor_joins_one", "A survivor joins your base."), + ("system.time.event.survivor_joins_many", "{count} survivors join your base."), + ("system.time.event.base_max_capacity", "Your base is at maximum capacity."), + ("system.time.event.invasion_ended", "The {enemy} invasion has ended."), + ("system.time.event.blessing_speed_fades", "The speed blessing fades."), + ("system.time.event.blessing_resident_fades", "The residents' purpose fades."), + ("system.time.event.blessing_search_fades", "The eagle's sight fades."), + ("system.time.event.sun_setting", "The sun is setting."), + ("system.time.event.sky_brightening", "The sky begins to brighten."), + ("system.time.event.residents_repaired_barricade", "Residents repaired the barricade. +{health} health."), + ("system.time.event.incense_burned_out", "The incense has burned out."), + ("system.combat.burning", "Burning! {health} health remaining."), + ("system.combat.health_status", "{health} health."), + ("system.combat.you_died", "You have died."), + ("system.enemies.residents_killed_attacker", "Residents killed an attacking {enemy}."), + ("system.enemies.resident_taken", "A resident has been taken."), + ("system.action.fire.intensity.giant_blaze", "Giant blaze"), + ("system.action.fire.intensity.small_fire", "Small fire"), + ("system.action.fire.intensity.glowing_coals", "A few glowing coals"), + ("system.action.fire.estimate.less_than_one_hour", "less than 1 hour"), + ("system.action.fire.estimate.approximately_one_hour", "approximately 1 hour"), + ("system.action.fire.estimate.approximately_hours", "approximately {hours} hours"), + ("system.action.fire.status", "{label}. {estimate} remaining."), + ("system.action.no_fire_nearby", "No fire nearby."), + ("system.action.no_snare_nearby", "No snare nearby."), + ("system.action.snare_holds", "Snare holds a {catch_type}."), + ("system.action.snare_set_inactive", "Snare is set but not active yet."), + ("system.action.snare_set_empty", "Snare is set and empty."), + ("system.action.fish_on_line", "Fish on the line."), + ("system.action.no_fish_on_line", "No fish on the line."), + ("system.action.cannot_place_snares_in_base", "Cannot place snares in the base area."), + ("system.action.snare_already_here", "There is already a snare here."), + ("system.action.snare_set", "Snare set."), + ("system.action.no_snares_to_place", "No snares to place."), + ("system.action.feed_fire.stick", "You dump an arm load of sticks into the fire."), + ("system.action.feed_fire.vine", "You toss a few vines and leaves into the fire."), + ("system.action.feed_fire.log", "You heave a log into the fire."), + ("system.action.no_altar_built", "No altar built."), + ("system.action.need_clay_pot_for_incense", "You need a clay pot to burn incense."), + ("system.action.no_incense_to_burn", "No incense to burn."), + ("system.action.incense_burning", "Incense burning. {hours} hours remaining."), + ("system.action.no_sticks_to_feed_fire", "No sticks to feed fire."), + ("system.action.dumped_sticks", "Dumped {amount} sticks into the fire."), + ("system.action.no_vines_to_feed_fire", "No vines to feed fire."), + ("system.action.dumped_vines", "Dumped {amount} vines into the fire."), + ("system.action.no_logs_to_feed_fire", "No logs to feed fire."), + ("system.action.dumped_logs", "Dumped {amount} logs into the fire."), + ("system.action.burned_incense", "Burned {amount} incense. +{hours} hours."), + ("system.action.option.place_snare", "Place Snare"), + ("system.action.option.feed_fire_stick", "Feed fire with stick"), + ("system.action.option.feed_fire_vine", "Feed fire with vine"), + ("system.action.option.feed_fire_log", "Feed fire with log"), + ("system.action.option.burn_incense", "Burn incense"), + ("system.action.option.check_fire", "Check fire"), + ("system.action.option.check_snare", "Check snare"), + ("system.action.option.check_fishing_pole", "Check fishing pole"), + ("system.action.menu.prompt", "Action menu. {option}"), + ("system.action.cant_do_that", "Can't do that."), + ("system.base_info.not_in_base", "You are not in the base."), + ("system.base_info.menu.no_options", "Base info. No options."), + ("system.base_info.menu.prompt", "Base info. {option}"), + ("system.base.barricade_fallen", "The barricade has fallen!"), + ("system.base.no_food_residents_hungry", "No food, residents are hungry."), + ("system.base.resident_fishing_pole_broke_one", "A resident's fishing pole broke."), + ("system.base.resident_fishing_pole_broke_many", "{count} fishing poles broke."), + ("system.base.resident_caught_fish_one", "Resident caught a fish and added it to storage."), + ("system.base.resident_caught_fish_many", "Residents caught {count} fish and added them to storage."), + ("system.base.resident_smoked_fish", "Resident smoked a fish into {yield} smoked fish."), + ("system.base.livestock_produced", + "Livestock produced {meat} meat, {skins} skins, and {feathers} feathers and added to storage."), + ("system.base.resident_spear_broke_one", "A resident's spear broke from wear."), + ("system.base.resident_spear_broke_many", "{count} spears broke from wear."), + ("system.base.resident_sling_broke_one", "A resident's sling broke from wear."), + ("system.base.resident_sling_broke_many", "{count} slings broke from wear."), + ("system.base.resident_bow_broke_one", "A resident's bow broke from wear."), + ("system.base.resident_bow_broke_many", "{count} bows broke from wear."), + ("system.base.resident_clothing_repair_one", + "A resident is mending clothing. Used {vines} vines, {skins} skins, and {down} down."), + ("system.base.resident_clothing_repair_many", + "Residents are mending clothing. Used {vines} vines, {skins} skins, and {down} down."), + ("system.base.snare_escape_during_check", + "A {catch_type} escaped while a resident checked the snare at x {position}."), + ("system.base.resident_retrieved_from_snare", + "Resident retrieved {game_type} from snare at x {position} y 0 and reset it."), + ("system.base.resident_knife_broke_butchering", "A resident's knife broke while butchering."), + ("system.base.resident_butchered_result", + "Resident butchered {game_type}. Added {meat} meat, {skins} skins, {feathers} feathers, {down} down, {sinew} sinew to storage."), + ("system.base.resident_added_to_storage", "Resident added {item} to storage."), + ("system.base.resident_basket_broke_one", "A resident's basket broke."), + ("system.base.resident_basket_broke_many", "{count} baskets broke."), + ("system.base.resident_basket_broke_foraging_one", "A resident's basket broke while foraging."), + ("system.base.resident_basket_broke_foraging_many", "{count} baskets broke while foraging."), + ("system.base.resident_gathered_basket_food_one", "Resident gathered a basket of fruits and nuts."), + ("system.base.resident_gathered_basket_food_many", + "Residents gathered {count} baskets of fruits and nuts."), + ("system.fishing.size.small", "small"), + ("system.fishing.size.medium", "medium sized"), + ("system.fishing.size.large", "large"), + ("system.fishing.size.monster", "monster"), + ("system.fishing.pole_broke.default", "Your fishing pole broke."), + ("system.fishing.pole_broke.switched_weapons", "You switched weapons and your fishing pole broke."), + ("system.fishing.pole_broke.moved_fish_got_away", "You moved and the fish got away. Your fishing pole broke."), + ("system.fishing.pole_broke.moved", "You moved and your fishing pole broke."), + ("system.fishing.require_pole_equipped", "You need a fishing pole equipped."), + ("system.fishing.require_near_stream", "You need to be within 2 tiles of a stream."), + ("system.fishing.fish_on_line", "A fish is on the line!"), + ("system.fishing.caught", "Caught a {size} {fish}."), + ("system.fishing.fish_slipped_off_water", "The fish slipped off in the water."), + ("system.fishing.fish_got_away", "The fish got away."), + ("system.fishing.fish_still_on_line", "The fish is still on the line."), + ("system.fishing.stopped", "You stop fishing."), + ("system.altar.must_be_in_base", "Must be in base to use altar."), + ("system.altar.no_altar_built", "No altar built."), + ("system.altar.runed_cannot_sacrifice", "Runed items cannot be sacrificed."), + ("system.altar.nothing_to_sacrifice", "Nothing to sacrifice."), + ("system.altar.sacrificed_one", "Sacrificed 1 {item}. Favor +{gain}. Total {total}."), + ("system.altar.sacrificed_many", "Sacrificed {count} {items}. Favor +{gain}. Total {total}."), + ("system.altar.menu.prompt", "Altar. Favor {favor}."), + ("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.storage.runed_item_name", "Runed {equipment} of {rune}"), + ("system.storage.deposited_one", "Deposited {name}."), + ("system.storage.deposited_many", "Deposited {amount} {item}."), + ("system.storage.withdrew_one", "Withdrew {name}."), + ("system.storage.withdrew_many", "Withdrew {amount} {item}."), + ("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.character.info.name_sex", "Name {name}, {sex}"), + ("system.character.info.name_unknown", "Name unknown"), + ("system.character.info.health", "Health {health} of {max}"), + ("system.character.info.weapon", "Weapon {weapon}"), + ("system.character.info.clothing_equipped", "Clothing equipped: {items}"), + ("system.character.info.no_clothing", "No clothing equipped."), + ("system.character.info.missing", "Missing: {slots}"), + ("system.character.info.favor", "Favor {favor}"), + ("system.character.info.speed", "Speed {status}"), + ("system.character.info.pet", "Pet {pet}, health {health} of {max_health}, loyalty {loyalty} of {max_loyalty}"), + ("system.character.info.pet_knocked_out", "Knocked out for {hours} {hour_label}."), + ("system.character.info.pet_none", "No pet."), + ("system.character.word.hour", "hour"), + ("system.character.word.hours", "hours"), + ("system.pet.default_name", "pet"), + ("system.pet.returned", "A {pet} returns to you."), + ("system.pet.knocked_out", "Your {pet} has been knocked out."), + ("system.pet.knocked_out_with_remaining", "Your {pet} has been knocked out. {hours} {hour_label} remaining."), + ("system.pet.leaves", "{pet} leaves."), + ("system.pet.leaving", "Your {pet} is leaving."), + ("system.pet.on_the_way", "Your pet is on its way."), + ("system.pet.hungry_unresponsive", "A {pet} is hungry and unresponsive."), + ("system.pet.joins_you", "A {pet} joins you."), + ("system.pet.offer.prompt", "A friendly looking {pet} begs for food. Accept?"), + ("system.pet.offer.declined", "Declined."), + ("system.pet.retrieve.cant_carry_chew_on", + "You can't carry a {item}, so you give it to a {pet} to chew on."), + ("system.pet.retrieve.retrieved_item", "Your {pet} retrieved {item}."), + ("system.pet.retrieve.random_find", "Your {pet} retrieved {count} {item}."), + ("system.pet.recovered_from_injuries", "Your {pet} has recovered from its injuries."), + ("system.pet.getting_hungry", "A {pet} is getting hungry."), + ("system.fylgja.already_used_today", "You have already used your Fylgja today."), + ("system.fylgja.menu.prompt", "Fylgja menu."), + ("system.fylgja.need_open_ground_to_charge", "You need open ground in front of you to charge."), + ("system.fylgja.option", "{name} Fylgja"), + ("system.fylgja.connection_with_target", "You have a {stage} connection with the {target}."), + ("system.fylgja.unlocked", "You have unlocked the {name} Fylgja!"), + ("system.fylgja.already_unlocked", "You have already unlocked the {name} Fylgja."), + ("system.fylgja.name.unicorn", "Unicorn"), + ("system.fylgja.target.unicorn", "unicorn"), + ("system.fylgja.stage.tenuous", "tenuous"), + ("system.fylgja.stage.faint", "faint"), + ("system.fylgja.stage.stirring", "stirring"), + ("system.fylgja.stage.budding", "budding"), + ("system.fylgja.stage.kindled", "kindled"), + ("system.fylgja.stage.bound", "bound"), + ("system.fylgja.stage.sworn", "sworn"), + ("system.fylgja.stage.ascendant", "ascendant"), + ("system.fylgja.stage.ultimate", "ultimate"), + ("system.quest.name.bat_invasion", "Bat Invasion"), + ("system.quest.name.catch_boomerang", "Catch the Boomerang"), + ("system.quest.name.enchanted_melody", "Enchanted Melody"), + ("system.quest.name.escape_from_hel", "Escape from Hel"), + ("system.quest.name.skeletal_bard", "Skeletal Bard"), + ("system.quest.name.unknown", "Unknown Quest"), + ("system.quest.description.bat_invasion", + "Bat Invasion. Giant killer bats are attacking. Press space to throw when the bat is centered."), + ("system.quest.description.catch_boomerang", + "Catch the Boomerang. Throw and catch the boomerang as it returns for up to 4 points."), + ("system.quest.description.enchanted_melody", + "Enchanted Melody. Repeat the pattern using E R D F or U I J K. Lowest to highest pitch."), + ("system.quest.description.escape_from_hel", + "Escape from Hel. Press space to jump over open graves. The pace quickens."), + ("system.quest.description.skeletal_bard", + "Skeletal Bard. A skeleton named Billy Bones is practicing to become a bard. Count the notes."), + ("system.quest.description.unknown", "Unknown quest."), + ("system.quest.new_available", "A new quest is available: {quest}."), + ("system.quest.none_available", "No quests available."), + ("system.quest.favor_phrase.displeased", "The gods are displeased with your performance."), + ("system.quest.favor_phrase.pleased", "The gods are pleased with your performance."), + ("system.quest.favor_phrase.very_pleased", "The gods are very pleased with your performance."), + ("system.quest.favor_phrase.extremely_pleased", "The gods are extremely pleased with your performance."), + ("system.quest.favor_phrase.ultimately_pleased", "The gods are ultimately pleased with your performance."), + ("system.quest.rewards.title", "Quest Rewards"), + ("system.quest.rewards.complete_heading", "Quest Complete!"), + ("system.quest.rewards.heading", "Rewards:"), + ("system.quest.rewards.favor_line", "Favor: +{favor}"), + ("system.quest.rewards.item_line", "{item}: +{amount}"), + ("system.quest.rewards.inventory_full_line", "Inventory full, could not receive {item}."), + ("system.quest.rewards.no_items_awarded", "No items awarded."), + ("system.quest.rewards.score_line", "Score: {score}"), + ("system.quest.menu.prompt", "Quest menu."), + ("system.quest.menu.count", "{index} of {total}"), + ("system.quest.minigame.starting", "Starting."), + ("system.quest.bat_invasion.complete_score", "Bat invasion complete. Score {score}."), + ("system.quest.boomerang.press_space_to_throw", "Press Space to throw."), + ("system.quest.boomerang.skill.almost_dropped", "almost dropped"), + ("system.quest.boomerang.skill.managed_to_catch", "managed to catch"), + ("system.quest.boomerang.skill.caught", "caught"), + ("system.quest.boomerang.skill.expertly_caught", "expertly caught"), + ("system.quest.boomerang.caught_result", "You {skill} the boomerang. {points} points.{suffix}"), + ("system.quest.boomerang.missed", "You missed the boomerang.{suffix}"), + ("system.quest.boomerang.hit_you", "The boomerang hit you.{suffix}"), + ("system.quest.boomerang.summary", "You caught {count} boomerangs for a total of {score} points."), + ("system.quest.enchanted_melody.practice_prompt", "Practice mode. Press Enter to begin, Escape to cancel."), + ("system.quest.enchanted_melody.starting_prompt", "Starting. Repeat the pattern."), + ("system.quest.enchanted_melody.score", "You matched {rounds} notes. Score {score}."), + ("system.quest.skeletal_bard.select_notes", "Select number of notes. {selection}."), + ("system.quest.skeletal_bard.round_result", + "You entered {guess} notes and the actual number was {actual}. {points} points. Press Enter to continue."), + ("system.quest.skeletal_bard.complete_score", "Skeletal Bard complete. Your score for all five tunes is {score}."), + ("system.quest.escape_from_hel.fell_in_score", "You fell in. Score {score}."), + ("system.adventure.none_in_base", "No adventures available in the base."), + ("system.adventure.already_attempted_today", "You have already attempted an adventure today."), + ("system.adventure.option.unicorn_hunt", "Unicorn Hunt (Mountain Boss)"), + ("system.adventure.option.bandit_hideout", "Bandit's Hideout"), + ("system.adventure.none_in_area", "No adventures found in this area."), + ("system.adventure.menu.prompt", "Adventure menu."), + ("system.adventure.victory", "Victory!"), + ("system.adventure.rewards.title", "=== Victory Rewards ==="), + ("system.adventure.unicorn.flee", "You fled from the unicorn."), + ("system.adventure.unicorn.position_report", + "X {x}, {terrain}, facing {direction}"), + ("system.adventure.unicorn.player_trampled", "The unicorn trampled you!"), + ("system.adventure.unicorn.victory_title", "Unicorn Victory"), + ("system.adventure.unicorn.reward.favor_awarded", + "The gods are pleased with your victory! {favor} favor awarded."), + ("system.adventure.unicorn.reward.heal_scrolls", "Heal Scrolls: +{count}."), + ("system.adventure.unicorn.reward.learned_rune_swiftness", "Learned Rune of Swiftness!"), + ("system.adventure.unicorn.reward.engrave_unlocked", + "You can now engrave equipment with this rune at the crafting menu."), + ("system.adventure.unicorn.reward.mastered_rune_swiftness", + "You have already mastered the Rune of Swiftness."), + ("system.adventure.unicorn.horses.no_stable_or_storage", + "Stable or storage not built. No horses were captured."), + ("system.adventure.unicorn.horses.stable_full", "Stable is full. No horses were captured."), + ("system.adventure.unicorn.horses.joined_stable", "A horse joins your stable."), + ("system.adventure.unicorn.horses.none_captured", "No horses were captured."), + ("system.adventure.bandit_hideout.flee", "You fled from the bandit hideout."), + ("system.adventure.bandit_hideout.position_report", + "X {x}, {terrain}. Distance to base {distance}. Facing {direction}. Base health {health} of {max}."), + ("system.adventure.bandit_hideout.player_defeated", "You were defeated in the bandit hideout."), + ("system.character.report.position", "{direction}, x {x}, y {y}, terrain {terrain}"), + ("system.character.report.terrain_mountains", "{terrain}. Mountains"), + ("system.character.report.player", "{health} of {max} health"), + ("system.character.report.pet", + "{pet}: {health} of {max} health, loyalty {loyalty} of {max_loyalty}"), + ("system.item.label.items", "items"), + ("system.item.label.item", "item"), + ("system.item.label.unknown", "Unknown"), + ("system.equipment.menu.prompt", "Equipment menu. {option}"), + ("system.equipment.menu.nothing_to_equip", "Nothing to equip."), + ("system.equipment.menu.equipped_suffix", " (equipped)"), + ("system.equipment.set_to_slot", "{name} set to slot {slot}."), + ("system.equipment.equipped", "{name} equipped."), + ("system.equipment.unequipped", "{name} unequipped."), + ("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.environment.fall.damage_report", + "Fell {height} feet! Took {damage} damage. {health} health remaining."), + ("system.environment.snare.collected_with_catch", "Collected {catch_type} and snare."), + ("system.environment.snare.collected_empty", "Collected snare."), + ("system.environment.tree.cut_down", "This tree has been cut down."), + ("system.environment.tree.empty", "This tree is empty."), + ("system.environment.area.nothing_left", "This area has nothing left."), + ("system.environment.tree.climb_started", "Started climbing tree. Height is {height} feet."), + ("system.environment.tree.climb_reached_top", "Reached the top at {height} feet."), + ("system.environment.tree.climb_reached_ground", "Safely reached the ground."), + ("system.environment.tree.climb_down", "Climbing down."), + ("system.environment.movement.need_rope", "You'll need a rope to climb there."), + ("system.environment.movement.press_up_climb", "Press up to climb up."), + ("system.environment.movement.press_down_climb", "Press down to climb down."), + ("system.environment.movement.need_canoe", "You need a canoe to cross deep water."), + ("system.environment.rope_climb.direction.up", "up"), + ("system.environment.rope_climb.direction.down", "down"), + ("system.environment.rope_climb.progress", "Climbing {direction}. {distance} feet."), + ("system.environment.rope_climb.reached_elevation", "Reached elevation {elevation}."), + ("system.snare.escape_at", "A {catch_type} escaped from your snare at x {position} y 0!"), + ("system.snare.caught_at", "{catch_type} caught in snare at x {position} y 0!"), + ("system.snare.stepped_on_with_escape", "You stepped on your snare! The {catch_type} escaped."), + ("system.snare.stepped_on_broke", "You stepped on your snare and broke it!"), + ("system.fire.low_fuel_at", "Fire at x {position} y {y} is getting low!"), + ("system.fire.went_out_at", "Fire at x {position} y {y} has gone out."), + ("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.in_progress", "Crafting..."), + ("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.weapons.crafted.spear", "Crafted a Spear."), + ("system.crafting.weapons.crafted.sling", "Crafted a Sling."), + ("system.crafting.weapons.crafted.bow", "Crafted a Bow."), + ("system.crafting.weapons.crafted.stone_axe", "Crafted a Stone Axe."), + ("system.crafting.weapons.crafted_max.spears", "Crafted {count} Spears."), + ("system.crafting.weapons.crafted_max.slings", "Crafted {count} Slings."), + ("system.crafting.weapons.crafted_max.bows", "Crafted {count} Bows."), + ("system.crafting.weapons.crafted_max.stone_axes", "Crafted {count} Stone Axes."), + ("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.tools.crafted.stone_knife", "Crafted a Stone Knife."), + ("system.crafting.tools.crafted.snare", "Crafted a Snare."), + ("system.crafting.tools.crafted.fishing_pole", "Crafted a Fishing Pole."), + ("system.crafting.tools.crafted.rope", "Crafted rope."), + ("system.crafting.tools.crafted.quiver", "Crafted a Quiver."), + ("system.crafting.tools.crafted.canoe", "Crafted a Canoe."), + ("system.crafting.tools.crafted.reed_basket", "Crafted a reed basket."), + ("system.crafting.tools.crafted.clay_pot", "Crafted a clay pot."), + ("system.crafting.tools.crafted_max.stone_knives", "Crafted {count} Stone Knives."), + ("system.crafting.tools.crafted_max.snares", "Crafted {count} Snares."), + ("system.crafting.tools.crafted_max.fishing_poles", "Crafted {count} Fishing Poles."), + ("system.crafting.tools.crafted_max.ropes", "Crafted {count} Rope."), + ("system.crafting.tools.crafted_max.quivers", "Crafted {count} Quivers."), + ("system.crafting.tools.crafted_max.canoes", "Crafted {count} Canoes."), + ("system.crafting.tools.crafted_max.reed_baskets", "Crafted {count} Reed Baskets."), + ("system.crafting.tools.crafted_max.clay_pots", "Crafted {count} Clay Pots."), + ("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.materials.not_enough_quiver_capacity_for_arrows", "Not enough quiver capacity for arrows."), + ("system.crafting.materials.crafted.arrows", "Crafted {count} arrows."), + ("system.crafting.materials.crafted.bowstring", "Crafted a bowstring."), + ("system.crafting.materials.crafted_max.bowstrings", "Crafted {count} Bowstrings."), + ("system.crafting.materials.crafted.incense", "Crafted incense."), + ("system.crafting.materials.crafted_max.incense", "Crafted {count} Incense."), + ("system.crafting.materials.smoke_fish.crafted", "Smoked a fish into {yield} smoked fish."), + ("system.crafting.materials.smoke_fish.crafted_max", + "Smoked {count} fish into {yield} smoked fish."), + ("system.crafting.materials.butcher.goose", "Butchered goose. Got 1 meat, feathers, and down."), + ("system.crafting.materials.butcher.turkey", "Butchered turkey. Got 1 meat and feathers."), + ("system.crafting.materials.butcher.boar", "Butchered boar. Got meat, 3 skins, and 2 sinew."), + ("system.crafting.materials.butcher.default", "Butchered {game}. Got 1 meat and 1 skin."), + ("system.crafting.materials.butcher.no_space_outputs", "No space for outputs."), + ("system.crafting.materials.butcher.max_result", + "Butchered {count} game. Got {meat} meat, {skins} skins, {feathers} feathers, {down} down, {sinew} sinew."), + ("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.clothing.crafted.skin_hat", "Crafted a Skin Hat."), + ("system.crafting.clothing.crafted.skin_gloves", "Crafted Skin Gloves."), + ("system.crafting.clothing.crafted.skin_pants", "Crafted Skin Pants."), + ("system.crafting.clothing.crafted.skin_tunic", "Crafted a Skin Tunic."), + ("system.crafting.clothing.crafted.moccasins", "Crafted moccasins."), + ("system.crafting.clothing.crafted.skin_pouch", "Crafted a Skin Pouch."), + ("system.crafting.clothing.crafted.backpack", "Crafted a Backpack."), + ("system.crafting.clothing.crafted_max.skin_hats", "Crafted {count} Skin Hats."), + ("system.crafting.clothing.crafted_max.skin_gloves", "Crafted {count} Skin Gloves."), + ("system.crafting.clothing.crafted_max.skin_pants", "Crafted {count} Skin Pants."), + ("system.crafting.clothing.crafted_max.skin_tunics", "Crafted {count} Skin Tunics."), + ("system.crafting.clothing.crafted_max.moccasins", "Crafted {count} Moccasins."), + ("system.crafting.clothing.crafted_max.skin_pouches", "Crafted {count} Skin Pouches."), + ("system.crafting.clothing.crafted_max.backpacks", "Crafted {count} Backpacks."), + ("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.buildings.none_available", "No buildings available."), + ("system.crafting.buildings.firepit.already_in_base", "There is already a firepit in the base."), + ("system.crafting.buildings.firepit.already_here", "There is already a firepit here."), + ("system.crafting.buildings.firepit.built", "Firepit built here."), + ("system.crafting.buildings.fire.requires_firepit_nearby", "You need a firepit within 2 tiles to build a fire."), + ("system.crafting.buildings.fire.already_in_base", "There is already a fire in the base."), + ("system.crafting.buildings.fire.built", "Fire built at firepit."), + ("system.crafting.buildings.herb_garden.base_only", "Herb garden can only be built in the base area."), + ("system.crafting.buildings.herb_garden.already_in_base", "There is already an herb garden in the base."), + ("system.crafting.buildings.herb_garden.built", "Herb garden built. The base now heals faster."), + ("system.crafting.buildings.storage.base_only", "Storage must be built in the base."), + ("system.crafting.buildings.storage.fully_upgraded", "Storage is fully upgraded."), + ("system.crafting.buildings.storage.upgraded", "Storage upgraded. Capacity is now {capacity} per item."), + ("system.crafting.buildings.pasture.base_only", "Pasture must be built in the base."), + ("system.crafting.buildings.pasture.requires_storage_upgrade", "Storage must be upgraded before a pasture."), + ("system.crafting.buildings.pasture.already_built", "Pasture already built."), + ("system.crafting.buildings.pasture.built", "Pasture built."), + ("system.crafting.buildings.stable.base_only", "Stable must be built in the base."), + ("system.crafting.buildings.stable.requires_storage_upgrade", "Storage must be upgraded before a stable."), + ("system.crafting.buildings.stable.already_built", "Stable already built."), + ("system.crafting.buildings.stable.built", "Stable built."), + ("system.crafting.buildings.altar.base_only", "Altar must be built in the base."), + ("system.crafting.buildings.altar.already_built", "Altar already built."), + ("system.crafting.buildings.altar.built", "Altar built."), + ("system.crafting.runes.base_only", "Rune engraving can only be done in the base area."), + ("system.crafting.runes.option", "{rune} (1 Clay, 1 Favor) [Requires Knife]"), + ("system.crafting.runes.none_unlocked", "No runes unlocked yet."), + ("system.crafting.runes.prompt", "Runes. {option}"), + ("system.crafting.runes.equipment_option", "{name} ({count} available)"), + ("system.crafting.runes.no_equipment_available", "No equipment available to engrave."), + ("system.crafting.runes.select_equipment_prompt", + "Select equipment to engrave with {rune}. {option}"), + ("system.crafting.runes.no_item_available", "No {item} available."), + ("system.crafting.runes.runed_name", "Runed {equipment} of {rune}"), + ("system.crafting.runes.engraved", "Engraved {item}."), + ("system.crafting.runes.engraved_max", "Engraved {count} {item} with {rune}."), + ("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.barricade.full_health", "Barricade is already at full health."), + ("system.crafting.barricade.no_materials", "No materials to reinforce the barricade."), + ("system.crafting.barricade.reinforced.sticks", + "Reinforced barricade with sticks. +{gained} health. Now {health} of {max}."), + ("system.crafting.barricade.reinforced.vines", + "Reinforced barricade with vines. +{gained} health. Now {health} of {max}."), + ("system.crafting.barricade.reinforced.log", + "Reinforced barricade with log. +{gained} health. Now {health} of {max}."), + ("system.crafting.barricade.reinforced.stones", + "Reinforced barricade with stones. +{gained} health. Now {health} of {max}."), + ("system.crafting.barricade.reinforced_max.sticks", + "Reinforced barricade {count} times with sticks. Health now {health}."), + ("system.crafting.barricade.reinforced_max.vines", + "Reinforced barricade {count} times with vines. Health now {health}."), + ("system.crafting.barricade.reinforced_max.log", + "Reinforced barricade {count} times with log. Health now {health}."), + ("system.crafting.barricade.reinforced_max.stones", + "Reinforced barricade {count} times with stones. Health now {health}."), + ("system.crafting.barricade.not_enough.sticks", "Not enough sticks."), + ("system.crafting.barricade.not_enough.vines", "Not enough vines."), + ("system.crafting.barricade.not_enough.logs", "Not enough logs."), + ("system.crafting.barricade.not_enough.stones", "Not enough stones."), + ("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"), + ("system.story.voice.format", "{speaker}: {line}"), + ("system.story.god.fallback", "A god"), + ("system.story.god.odin", "Odin"), + ("system.story.god.thor", "Thor"), + ("system.story.god.freyja", "Freyja"), + ("system.story.god.loki", "Loki"), + ("system.story.god.tyr", "Tyr"), + ("system.story.god.baldur", "Baldur"), + ("system.story.god.frigg", "Frigg"), + ("system.story.god.heimdall", "Heimdall"), + ("system.story.god.hel", "Hel"), + ("system.story.god.fenrir", "Fenrir"), + ("system.story.god.freyr", "Freyr"), + ("system.story.line01", "The world has fallen."), + ("system.story.line02", "Once-great cities lie in ruin."), + ("system.story.line03", "In most places, not two stones remain as mankind set them."), + ("system.story.line04", "The gods, long thought sleeping, achieved the impossible."), + ("system.story.line05", "Ragnarok was averted."), + ("system.story.line06", "Great Fenrir was freed and did not strike down his captors."), + ("system.story.line07", "Surtr did not lay waste to our world with his flaming greatsword."), + ("system.story.line08", "For a fleeting age, the gods wrestled fate into silence."), + ("system.story.line09", "But the end of the world was delayed, never denied."), + ("system.story.line10", "One force lay beyond even divine command: mankind."), + ("system.story.line11", "Hatred swelled."), + ("system.story.line12", "Greed answered it."), + ("system.story.line13", "Mercy withered."), + ("system.story.line14", "Differences became crimes."), + ("system.story.line15", "Folk no longer argued; they tore at one another like beasts."), + ("system.story.line16", "Neighbors became enemies for trifles: a color, a song, a single word."), + ("system.story.line17", "War followed."), + ("system.story.line18", "Cities burned, roads broke, and monsters wearing human faces roamed the streets."), + ("system.story.line19", "Then ruin deepened into terror."), + ("system.story.line20", "The dead, enraged by wanton slaughter, rose from their graves."), + ("system.story.line21", "Corpses woke and hunted the living."), + ("system.story.line22", "Night-stalkers fed on blood."), + ("system.story.line23", "Even ash and dust remembered form and rose again."), + ("system.story.line24", "Soon all that remained was rubble and bone."), + ("system.story.line25", "Your last memory is flight from a vast horde of nightmare creatures."), + ("system.story.line26", "When you wake, you are somewhere strange."), + ("system.story.line27", + "A crude wooden shelter stands against the wind, open to the elements, braced with sticks and logs."), + ("system.story.line28", "As your senses return, faint voices stir in the dark."), + ("system.story.line29", "After the death of this world, another shall rise."), + ("system.story.line30", "You have been judged worthy to try and survive."), + ("system.story.line31", "This world belongs to the dead."), + ("system.story.line32", "In time, they will slay even us."), + ("system.story.line33", "Prove yourself, warrior, and you may walk into the next world."), + ("system.story.line34", "May your death, when it comes, be honorable."), + ("system.story.line35", "We will aid you while we still have strength, but time is against us."), + ("system.story.line36", + "Beyond your shelter lies grassland, and farther off, broken stone that may once have been a city."), + ("system.story.line37", "In the total devastation, even memory has lost its shape."), + ] + + 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..79e1312 --- /dev/null +++ b/scripts/validate_i18n_catalog.py @@ -0,0 +1,152 @@ +#!/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 + + if not section: + full_key = key + elif section == "messages": + full_key = key if "." in key else f"{section}.{key}" + else: + section_prefix = f"{section}." + full_key = key if key.startswith(section_prefix) else f"{section_prefix}{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/dialog.ogg b/sounds/dialog.ogg new file mode 100644 index 0000000..49971cc --- /dev/null +++ b/sounds/dialog.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c08df4614917374bf113d4cdc71600c565368ea46290fb45182699ba14f9565f +size 5794 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/menu/menu_move.ogg b/sounds/menu/menu_move.ogg index cce3dbc..4f060f7 100644 --- a/sounds/menu/menu_move.ogg +++ b/sounds/menu/menu_move.ogg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cfff60d50e0bb5bcb47b449d6c349dd13bb02538a61de784e29908025801d269 -size 4081 +oid sha256:e04378d0b953d09a67ec1cbc57c27f3256c88b7817dd00c14c6e9aa3ea24d168 +size 3949 diff --git a/sounds/menu/menu_select.ogg b/sounds/menu/menu_select.ogg index 7c86ce0..7dcb624 100644 --- a/sounds/menu/menu_select.ogg +++ b/sounds/menu/menu_select.ogg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ae144ea519d33452915ca7fbd0ff81195a372cfa4f01846845c17dc66c85376b -size 5595 +oid sha256:fbc58963b9626ef174a77e587defe36d107b64638fe4b0e8fef6443f06c60b32 +size 5506 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 cfe8997..a84d7b9 100644 --- a/src/audio_utils.nvgt +++ b/src/audio_utils.nvgt @@ -1,5 +1,19 @@ -string get_footstep_sound(int current_x, int base_end, int grass_end) -{ +#include "libstorm-nvgt/volume_controls.nvgt" + +bool audio_asset_exists(const string& in soundFile) { + if (file_exists(soundFile)) { + return true; + } + + pack @activePack = cast(sound_default_pack); + if (@activePack != null && activePack.file_exists(soundFile)) { + return true; + } + + return false; +} + +string get_footstep_sound(int current_x, int base_end, int grass_end) { // Check if in water first (regular streams or mountain streams) if (is_deep_stream_at(current_x)) { return "sounds/terrain/deep_water.ogg"; @@ -13,25 +27,18 @@ string get_footstep_sound(int current_x, int base_end, int grass_end) return "sounds/terrain/hard_stone.ogg"; } - if (current_x <= base_end) - { + if (current_x <= base_end) { // Base area return "sounds/terrain/wood.ogg"; - } - else if (current_x <= grass_end) - { + } else if (current_x <= grass_end) { // Grass area return "sounds/terrain/grass.ogg"; - } - else if (current_x <= GRAVEL_END) - { + } else if (current_x <= GRAVEL_END) { // Gravel area return "sounds/terrain/gravel.ogg"; - } - else if (expanded_area_start != -1 && current_x >= expanded_area_start && current_x <= expanded_area_end) - { + } else if (expanded_area_start != -1 && current_x >= expanded_area_start && current_x <= expanded_area_end) { // Check for mountain terrain first - MountainRange@ mountain = get_mountain_at(current_x); + MountainRange @mountain = get_mountain_at(current_x); if (mountain !is null) { string terrain = mountain.get_terrain_at(current_x); if (terrain == "stone") { @@ -49,8 +56,7 @@ string get_footstep_sound(int current_x, int base_end, int grass_end) // Regular expanded area - check terrain type int index = current_x - expanded_area_start; - if (index >= 0 && index < int(expanded_terrain_types.length())) - { + if (index >= 0 && index < int(expanded_terrain_types.length())) { string terrain = expanded_terrain_types[index]; // Handle "mountain:terrain" format from older saves if (terrain.find("mountain:") == 0) { @@ -76,47 +82,39 @@ string get_footstep_sound(int current_x, int base_end, int grass_end) return "sounds/terrain/gravel.ogg"; } -void play_footstep(int current_x, int base_end, int grass_end) -{ +void play_footstep(int current_x, int base_end, int grass_end) { string sound_file = get_footstep_sound(current_x, base_end, grass_end); - if(file_exists(sound_file)) { + if (audio_asset_exists(sound_file)) { p.play_stationary(sound_file, false); } } -int to_audio_position(int tile_x) -{ +int to_audio_position(int tile_x) { return tile_x * AUDIO_TILE_SCALE; } -float to_audio_volume_step(float volume_step) -{ +float to_audio_volume_step(float volume_step) { return volume_step / float(AUDIO_TILE_SCALE); } -int play_1d_tile(string sound_file, int listener_x, int sound_x, bool looping, bool persistent = false) -{ +int play_1d_tile(string sound_file, int listener_x, int sound_x, bool looping, bool persistent = false) { return p.play_1d(sound_file, to_audio_position(listener_x), to_audio_position(sound_x), looping, persistent); } -bool update_sound_1d_tile(int slot, int sound_x) -{ +bool update_sound_1d_tile(int slot, int sound_x) { return p.update_sound_1d(slot, to_audio_position(sound_x)); } -void update_listener_tile(int listener_x) -{ +void update_listener_tile(int listener_x) { p.update_listener_1d(to_audio_position(listener_x)); } -void update_sound_range_1d_tile(int slot, int range_tiles) -{ +void update_sound_range_1d_tile(int slot, int range_tiles) { p.update_sound_range_1d(slot, range_tiles * AUDIO_TILE_SCALE, range_tiles * AUDIO_TILE_SCALE); } -int play_1d_with_volume_step(string sound_file, int listener_x, int sound_x, bool looping, float volume_step) -{ +int play_1d_with_volume_step(string sound_file, int listener_x, int sound_x, bool looping, float volume_step) { int slot = p.play_1d(sound_file, listener_x, sound_x, looping); if (slot != -1) { p.update_sound_positioning_values(slot, -1.0, volume_step, true); @@ -124,31 +122,29 @@ int play_1d_with_volume_step(string sound_file, int listener_x, int sound_x, boo return slot; } -void play_positional_footstep(int listener_x, int step_x, int base_end, int grass_end, int max_distance, float volume_step) -{ +void play_positional_footstep(int listener_x, int step_x, int base_end, int grass_end, int max_distance, + float volume_step) { if (abs(step_x - listener_x) > max_distance) { return; } string sound_file = get_footstep_sound(step_x, base_end, grass_end); - if(file_exists(sound_file)) { + if (audio_asset_exists(sound_file)) { play_1d_with_volume_step(sound_file, listener_x, step_x, false, volume_step); } } -void play_land_sound(int current_x, int base_end, int grass_end) -{ +void play_land_sound(int current_x, int base_end, int grass_end) { // Reusing the same logic to play the terrain sound on landing string sound_file = get_footstep_sound(current_x, base_end, grass_end); - if(file_exists(sound_file)) { + if (audio_asset_exists(sound_file)) { p.play_stationary(sound_file, false); } } -string get_item_collect_sound(string itemName) -{ +string get_item_collect_sound(string itemName) { string lookupName = itemName; if (lookupName == "sticks") { lookupName = "stick"; @@ -167,16 +163,15 @@ string get_item_collect_sound(string itemName) } string soundFile = "sounds/items/" + lookupName + ".ogg"; - if (file_exists(soundFile)) { + if (audio_asset_exists(soundFile)) { return soundFile; } return "sounds/items/miscellaneous.ogg"; } -void play_item_collect_sound(string itemName) -{ +void play_item_collect_sound(string itemName) { string soundFile = get_item_collect_sound(itemName); - if (file_exists(soundFile)) { + if (audio_asset_exists(soundFile)) { p.play_stationary(soundFile, false); } } @@ -190,54 +185,36 @@ string get_player_damage_sound() { void play_player_damage_sound() { string soundFile = get_player_damage_sound(); - if (file_exists(soundFile)) { + if (audio_asset_exists(soundFile)) { p.play_stationary(soundFile, false); } } // 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; - } +void safe_destroy_sound(int& inout handle) { + 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/base_system.nvgt b/src/base_system.nvgt index 2f8a4c0..cb81c29 100644 --- a/src/base_system.nvgt +++ b/src/base_system.nvgt @@ -1,6 +1,7 @@ // Base automation helpers int get_food_requirement() { - if (residents_count <= 0) return 0; + if (residents_count <= 0) + return 0; return residents_count; // 1 food per resident per 8 hours } @@ -13,7 +14,8 @@ bool has_any_storage_food() { } bool has_any_streams() { - if (world_streams.length() > 0) return true; + if (world_streams.length() > 0) + return true; for (uint i = 0; i < world_mountains.length(); i++) { if (world_mountains[i].stream_positions.length() > 0) { return true; @@ -36,49 +38,64 @@ int get_resident_effect_multiplier() { } int get_horse_success_bonus() { - if (world_stables.length() == 0) return 0; - if (world_storages.length() == 0) return 0; - if (horses_count <= 0) return 0; + if (world_stables.length() == 0) + return 0; + if (world_storages.length() == 0) + return 0; + if (horses_count <= 0) + return 0; int count = horses_count; - if (count > MAX_HORSES) count = MAX_HORSES; + if (count > MAX_HORSES) + count = MAX_HORSES; return count * HORSE_SUCCESS_BONUS_PER; } int get_horse_resident_cooldown_reduction() { - if (world_stables.length() == 0) return 0; - if (world_storages.length() == 0) return 0; - if (horses_count <= 0) return 0; + if (world_stables.length() == 0) + return 0; + if (world_storages.length() == 0) + return 0; + if (horses_count <= 0) + return 0; int count = horses_count; - if (count > MAX_HORSES) count = MAX_HORSES; + if (count > MAX_HORSES) + count = MAX_HORSES; int cooldown_range = RESIDENT_COMBAT_BASE_COOLDOWN - RESIDENT_COMBAT_TARGET_COOLDOWN; - if (cooldown_range <= 0) return 0; + if (cooldown_range <= 0) + return 0; return (cooldown_range * count) / MAX_HORSES; } int get_resident_success_chance(int base_chance) { int chance = base_chance * get_resident_effect_multiplier(); chance += get_horse_success_bonus(); - if (chance > 100) chance = 100; + if (chance > 100) + chance = 100; return chance; } int get_resident_break_chance(int base_chance) { - if (!blessing_resident_active) return base_chance; + if (!blessing_resident_active) + return base_chance; int reduced = base_chance / get_resident_effect_multiplier(); - if (reduced < 1 && base_chance > 0) reduced = 1; + if (reduced < 1 && base_chance > 0) + reduced = 1; return reduced; } int get_resident_escape_chance(int base_chance) { - if (!blessing_resident_active) return base_chance; + if (!blessing_resident_active) + return base_chance; int reduced = base_chance / get_resident_effect_multiplier(); - if (reduced < 1 && base_chance > 0) reduced = 1; + if (reduced < 1 && base_chance > 0) + reduced = 1; return reduced; } int get_resident_cooldown(int base_cooldown) { int cooldown = base_cooldown / get_resident_effect_multiplier(); - if (cooldown < 1) cooldown = 1; + if (cooldown < 1) + cooldown = 1; return cooldown; } @@ -100,7 +117,8 @@ int apply_resident_damage_bonus(int damage) { void consume_food_for_residents() { int needed = get_food_requirement(); - if (needed <= 0) return; + if (needed <= 0) + return; int meat_available = get_storage_count(ITEM_MEAT); int smoked_fish_available = get_storage_count(ITEM_SMOKED_FISH); int basket_food_available = get_storage_count(ITEM_BASKET_FOOD); @@ -126,21 +144,27 @@ void consume_food_for_residents() { set_storage_count(ITEM_SMOKED_FISH, 0); set_storage_count(ITEM_BASKET_FOOD, 0); if (x <= BASE_END) { - notify("No food, residents are hungry."); + notify(tr("system.base.no_food_residents_hungry")); } } } void attempt_resident_fishing() { - if (!is_daytime) return; - if (residents_count <= 0) return; - if (get_storage_count(ITEM_FISHING_POLES) <= 0) return; - if (!has_any_streams()) return; - if (get_storage_count(ITEM_FISH) >= get_storage_stack_limit()) return; + if (!is_daytime) + return; + if (residents_count <= 0) + return; + if (get_storage_count(ITEM_FISHING_POLES) <= 0) + return; + if (!has_any_streams()) + return; + if (get_storage_count(ITEM_FISH) >= get_storage_stack_limit()) + return; int active_fishers = residents_count; int poles = get_storage_count(ITEM_FISHING_POLES); - if (poles < active_fishers) active_fishers = poles; + if (poles < active_fishers) + active_fishers = poles; int caught = 0; int poles_broken = 0; @@ -152,8 +176,10 @@ void attempt_resident_fishing() { poles_broken++; continue; } - if (random(1, 100) > fishing_chance) continue; - if (get_storage_count(ITEM_FISH) >= get_storage_stack_limit()) break; + if (random(1, 100) > fishing_chance) + continue; + if (get_storage_count(ITEM_FISH) >= get_storage_stack_limit()) + break; add_storage_count(ITEM_FISH, 1); add_storage_fish_weight(random(FISH_WEIGHT_MIN, FISH_WEIGHT_MAX)); caught++; @@ -163,39 +189,53 @@ void attempt_resident_fishing() { if (poles_broken > 0) { add_storage_count(ITEM_FISHING_POLES, -poles_broken); if (x <= BASE_END) { - string msg = (poles_broken == 1) - ? "A resident's fishing pole broke." - : poles_broken + " fishing poles broke."; + string msg = tr("system.base.resident_fishing_pole_broke_one"); + if (poles_broken > 1) { + dictionary polesBrokenArgs; + polesBrokenArgs.set("count", poles_broken); + msg = trf("system.base.resident_fishing_pole_broke_many", polesBrokenArgs); + } speak_with_history(msg, true); } } if (caught > 0 && x <= BASE_END) { if (caught == 1) { - speak_with_history("Resident caught a fish and added it to storage.", true); + speak_with_history(tr("system.base.resident_caught_fish_one"), true); } else { - speak_with_history("Residents caught " + caught + " fish and added them to storage.", true); + dictionary caughtFishArgs; + caughtFishArgs.set("count", caught); + speak_with_history(trf("system.base.resident_caught_fish_many", caughtFishArgs), true); } } } void attempt_resident_fish_smoking() { - if (!is_daytime) return; - if (residents_count <= 0) return; - if (get_storage_count(ITEM_FISH) <= 0) return; - if (get_storage_count(ITEM_STICKS) <= 0) return; - if (!has_burning_fire_in_base()) return; + if (!is_daytime) + return; + if (residents_count <= 0) + return; + if (get_storage_count(ITEM_FISH) <= 0) + return; + if (get_storage_count(ITEM_STICKS) <= 0) + return; + if (!has_burning_fire_in_base()) + return; int attempts = get_resident_effect_multiplier(); int smoke_chance = get_resident_success_chance(RESIDENT_SMOKE_FISH_CHANCE); for (int attempt = 0; attempt < attempts; attempt++) { - if (get_storage_count(ITEM_FISH) <= 0) return; - if (get_storage_count(ITEM_STICKS) <= 0) return; - if (random(1, 100) > smoke_chance) continue; + if (get_storage_count(ITEM_FISH) <= 0) + return; + if (get_storage_count(ITEM_STICKS) <= 0) + return; + if (random(1, 100) > smoke_chance) + continue; int weight = (storage_fish_weights.length() > 0) ? storage_fish_weights[0] : get_default_fish_weight(); int yield = get_smoked_fish_yield(weight); - if (get_storage_count(ITEM_SMOKED_FISH) + yield > get_storage_stack_limit()) return; + if (get_storage_count(ITEM_SMOKED_FISH) + yield > get_storage_stack_limit()) + return; pop_storage_fish_weight(); add_storage_count(ITEM_FISH, -1); @@ -203,18 +243,24 @@ void attempt_resident_fish_smoking() { add_storage_count(ITEM_SMOKED_FISH, yield); if (x <= BASE_END) { - speak_with_history("Resident smoked a fish into " + yield + " smoked fish.", true); + dictionary smokedFishArgs; + smokedFishArgs.set("yield", yield); + speak_with_history(trf("system.base.resident_smoked_fish", smokedFishArgs), true); } } } void attempt_livestock_production() { - if (world_pastures.length() == 0) return; - if (world_storages.length() == 0) return; - if (livestock_count <= 0) return; + if (world_pastures.length() == 0) + return; + if (world_storages.length() == 0) + return; + if (livestock_count <= 0) + return; int count = livestock_count; - if (count > MAX_LIVESTOCK) count = MAX_LIVESTOCK; + if (count > MAX_LIVESTOCK) + count = MAX_LIVESTOCK; int meat_produced = 0; int skins_produced = 0; @@ -244,30 +290,19 @@ void attempt_livestock_production() { } if ((meat_produced > 0 || skins_produced > 0 || feathers_produced > 0) && x <= BASE_END) { - string msg = "Livestock produced "; - string[] outputs; - if (meat_produced > 0) outputs.insert_last(meat_produced + " meat"); - if (skins_produced > 0) outputs.insert_last(skins_produced + " skins"); - if (feathers_produced > 0) outputs.insert_last(feathers_produced + " feathers"); - - for (uint i = 0; i < outputs.length(); i++) { - if (i > 0) { - if (i == outputs.length() - 1) { - msg += " and "; - } else { - msg += ", "; - } - } - msg += outputs[i]; - } - msg += " and added to storage."; - speak_with_history(msg, true); + dictionary livestockArgs; + livestockArgs.set("meat", meat_produced); + livestockArgs.set("skins", skins_produced); + livestockArgs.set("feathers", feathers_produced); + speak_with_history(trf("system.base.livestock_produced", livestockArgs), true); } } void keep_base_fires_fed() { - if (residents_count <= 0) return; - if (get_storage_count(ITEM_VINES) <= 0 && get_storage_count(ITEM_STICKS) <= 0 && get_storage_count(ITEM_LOGS) <= 0) return; + if (residents_count <= 0) + return; + if (get_storage_count(ITEM_VINES) <= 0 && get_storage_count(ITEM_STICKS) <= 0 && get_storage_count(ITEM_LOGS) <= 0) + return; // Residents tend fires once per in-game hour from time_system. // Keep a 1-hour buffer above the 24-hour floor so fuel does not dip below 24 between hourly checks. @@ -278,9 +313,12 @@ void keep_base_fires_fed() { const int log_fuel_ms = 720000; // 12 hours for (uint i = 0; i < world_fires.length(); i++) { - if (world_fires[i].position > BASE_END) continue; - if (!world_fires[i].is_burning()) continue; - if (world_fires[i].fuel_remaining >= fire_target_ms) continue; + if (world_fires[i].position > BASE_END) + continue; + if (!world_fires[i].is_burning()) + continue; + if (world_fires[i].fuel_remaining >= fire_target_ms) + continue; while (world_fires[i].fuel_remaining < fire_target_ms) { int needed = fire_target_ms - world_fires[i].fuel_remaining; @@ -333,13 +371,15 @@ bool remove_random_stored_runed_weapon(int equipType) { for (uint i = 0; i < runeTypes.length(); i++) { total += get_stored_runed_item_count(equipType, runeTypes[i]); } - if (total <= 0) return false; + if (total <= 0) + return false; int roll = random(1, total); int running = 0; for (uint i = 0; i < runeTypes.length(); i++) { int count = get_stored_runed_item_count(equipType, runeTypes[i]); - if (count <= 0) continue; + if (count <= 0) + continue; running += count; if (roll <= running) { remove_stored_runed_item(equipType, runeTypes[i]); @@ -353,11 +393,13 @@ bool remove_random_stored_weapon(int equipType, int itemType) { int unrunedCount = get_storage_count(itemType); int runedCount = get_stored_runed_weapon_count(equipType); int total = unrunedCount + runedCount; - if (total <= 0) return false; + if (total <= 0) + return false; int roll = random(1, total); if (roll <= unrunedCount) { - if (unrunedCount > 0) add_storage_count(itemType, -1); + if (unrunedCount > 0) + add_storage_count(itemType, -1); return true; } return remove_random_stored_runed_weapon(equipType); @@ -379,26 +421,27 @@ int get_available_defense_weapons() { } bool can_residents_defend() { - if (residents_count <= 0) return false; + if (residents_count <= 0) + return false; return get_available_defense_weapons() > 0; } int choose_defense_weapon_type() { // Prefer bows if available - int bowCount = (get_storage_count(ITEM_ARROWS) > 0) - ? get_total_stored_weapon_count(EQUIP_BOW, ITEM_BOWS) - : 0; - if (bowCount > 0) return RESIDENT_WEAPON_BOW; + int bowCount = (get_storage_count(ITEM_ARROWS) > 0) ? get_total_stored_weapon_count(EQUIP_BOW, ITEM_BOWS) : 0; + if (bowCount > 0) + return RESIDENT_WEAPON_BOW; int spearCount = get_total_stored_weapon_count(EQUIP_SPEAR, ITEM_SPEARS); - int slingCount = (get_storage_count(ITEM_STONES) > 0) - ? get_total_stored_weapon_count(EQUIP_SLING, ITEM_SLINGS) - : 0; + int slingCount = (get_storage_count(ITEM_STONES) > 0) ? get_total_stored_weapon_count(EQUIP_SLING, ITEM_SLINGS) : 0; int total = spearCount + slingCount; - if (total == 0) return RESIDENT_WEAPON_SPEAR; - if (slingCount == 0) return RESIDENT_WEAPON_SPEAR; - if (spearCount == 0) return RESIDENT_WEAPON_SLING; + if (total == 0) + return RESIDENT_WEAPON_SPEAR; + if (slingCount == 0) + return RESIDENT_WEAPON_SPEAR; + if (spearCount == 0) + return RESIDENT_WEAPON_SLING; int roll = random(1, total); return (roll <= spearCount) ? RESIDENT_WEAPON_SPEAR : RESIDENT_WEAPON_SLING; @@ -407,18 +450,16 @@ int choose_defense_weapon_type() { timer resident_combat_timer; int perform_resident_defense(int target_pos) { - if (!can_residents_defend()) return 0; - if (resident_combat_timer.elapsed < get_resident_combat_cooldown()) return 0; + if (!can_residents_defend()) + return 0; + if (resident_combat_timer.elapsed < get_resident_combat_cooldown()) + return 0; // Choose weapon type (bows preferred, otherwise weighted by availability) int weapon_type = choose_defense_weapon_type(); - int bowCount = (get_storage_count(ITEM_ARROWS) > 0) - ? get_total_stored_weapon_count(EQUIP_BOW, ITEM_BOWS) - : 0; + int bowCount = (get_storage_count(ITEM_ARROWS) > 0) ? get_total_stored_weapon_count(EQUIP_BOW, ITEM_BOWS) : 0; int spearCount = get_total_stored_weapon_count(EQUIP_SPEAR, ITEM_SPEARS); - int slingCount = (get_storage_count(ITEM_STONES) > 0) - ? get_total_stored_weapon_count(EQUIP_SLING, ITEM_SLINGS) - : 0; + int slingCount = (get_storage_count(ITEM_STONES) > 0) ? get_total_stored_weapon_count(EQUIP_SLING, ITEM_SLINGS) : 0; int damage = 0; if (weapon_type == RESIDENT_WEAPON_BOW && bowCount > 0) { @@ -432,7 +473,8 @@ int perform_resident_defense(int target_pos) { damage = apply_resident_damage_bonus(RESIDENT_SPEAR_DAMAGE); // Weapons don't get consumed on use - they break via daily breakage check // Just play the sound - play_1d_with_volume_step("sounds/weapons/spear_swing.ogg", x, BASE_END + 1, false, RESIDENT_DEFENSE_VOLUME_STEP); + play_1d_with_volume_step("sounds/weapons/spear_swing.ogg", x, BASE_END + 1, false, + RESIDENT_DEFENSE_VOLUME_STEP); } else if (weapon_type == RESIDENT_WEAPON_SLING && slingCount > 0) { damage = apply_resident_damage_bonus(random(RESIDENT_SLING_DAMAGE_MIN, RESIDENT_SLING_DAMAGE_MAX)); // Slings use stones as ammo, so consume a stone @@ -450,15 +492,18 @@ int perform_resident_defense(int target_pos) { // Proactive resident ranged defense void attempt_resident_ranged_defense() { // Only if residents exist and have ranged weapons - if (residents_count <= 0) return; + if (residents_count <= 0) + return; int bowCount = get_total_stored_weapon_count(EQUIP_BOW, ITEM_BOWS); int slingCount = get_total_stored_weapon_count(EQUIP_SLING, ITEM_SLINGS); bool has_bow = (bowCount > 0 && get_storage_count(ITEM_ARROWS) > 0); bool has_sling = (slingCount > 0 && get_storage_count(ITEM_STONES) > 0); - if (!has_bow && !has_sling) return; + if (!has_bow && !has_sling) + return; // Shared cooldown for all resident combat actions - if (resident_combat_timer.elapsed < get_resident_combat_cooldown()) return; + if (resident_combat_timer.elapsed < get_resident_combat_cooldown()) + return; int range = has_bow ? BOW_RANGE : SLING_RANGE; // Find nearest enemy within range @@ -489,7 +534,8 @@ void attempt_resident_ranged_defense() { } // No targets in range - if (targetPos == -1) return; + if (targetPos == -1) + return; // Shoot! resident_combat_timer.restart(); @@ -523,13 +569,15 @@ void attempt_resident_ranged_defense() { } void process_daily_weapon_breakage() { - if (residents_count <= 0) return; + if (residents_count <= 0) + return; int spearTotal = get_total_stored_weapon_count(EQUIP_SPEAR, ITEM_SPEARS); int slingTotal = get_total_stored_weapon_count(EQUIP_SLING, ITEM_SLINGS); int bowTotal = get_total_stored_weapon_count(EQUIP_BOW, ITEM_BOWS); int totalWeapons = spearTotal + slingTotal + bowTotal; - if (totalWeapons == 0) return; + if (totalWeapons == 0) + return; // Number of breakage checks = min(residents, weapons) int checksToPerform = (residents_count < totalWeapons) ? residents_count : totalWeapons; @@ -545,7 +593,8 @@ void process_daily_weapon_breakage() { int remainingBows = bowTotal - bowChecks; int remaining = remainingSpears + remainingSlings + remainingBows; - if (remaining <= 0) break; + if (remaining <= 0) + break; int roll = random(1, remaining); if (roll <= remainingSpears && remainingSpears > 0) { @@ -586,9 +635,12 @@ void process_daily_weapon_breakage() { for (int i = 0; i < spearsBroken; i++) { remove_random_stored_weapon(EQUIP_SPEAR, ITEM_SPEARS); } - string msg = (spearsBroken == 1) - ? "A resident's spear broke from wear." - : spearsBroken + " spears broke from wear."; + string msg = tr("system.base.resident_spear_broke_one"); + if (spearsBroken > 1) { + dictionary spearsBrokenArgs; + spearsBrokenArgs.set("count", spearsBroken); + msg = trf("system.base.resident_spear_broke_many", spearsBrokenArgs); + } notify(msg); } @@ -596,9 +648,12 @@ void process_daily_weapon_breakage() { for (int i = 0; i < slingsBroken; i++) { remove_random_stored_weapon(EQUIP_SLING, ITEM_SLINGS); } - string msg = (slingsBroken == 1) - ? "A resident's sling broke from wear." - : slingsBroken + " slings broke from wear."; + string msg = tr("system.base.resident_sling_broke_one"); + if (slingsBroken > 1) { + dictionary slingsBrokenArgs; + slingsBrokenArgs.set("count", slingsBroken); + msg = trf("system.base.resident_sling_broke_many", slingsBrokenArgs); + } notify(msg); } @@ -606,15 +661,19 @@ void process_daily_weapon_breakage() { for (int i = 0; i < bowsBroken; i++) { remove_random_stored_weapon(EQUIP_BOW, ITEM_BOWS); } - string msg = (bowsBroken == 1) - ? "A resident's bow broke from wear." - : bowsBroken + " bows broke from wear."; + string msg = tr("system.base.resident_bow_broke_one"); + if (bowsBroken > 1) { + dictionary bowsBrokenArgs; + bowsBrokenArgs.set("count", bowsBroken); + msg = trf("system.base.resident_bow_broke_many", bowsBrokenArgs); + } notify(msg); } } void attempt_resident_clothing_repairs() { - if (residents_count <= 0) return; + if (residents_count <= 0) + return; int threshold = get_storage_stack_limit() / 2; if (threshold < RESIDENT_CLOTHING_REPAIR_COST) { @@ -648,7 +707,8 @@ void attempt_resident_clothing_repairs() { best_count = down_count; } - if (best_item == -1) break; + if (best_item == -1) + break; add_storage_count(best_item, -RESIDENT_CLOTHING_REPAIR_COST); repairs_done++; @@ -663,29 +723,13 @@ void attempt_resident_clothing_repairs() { } if (repairs_done > 0 && x <= BASE_END) { - string msg = (repairs_done == 1) - ? "A resident is mending clothing." - : "Residents are mending clothing."; - string[] materials; - if (vines_used > 0) materials.insert_last(vines_used + " vines"); - if (skins_used > 0) materials.insert_last(skins_used + " skins"); - if (down_used > 0) materials.insert_last(down_used + " down"); - - if (materials.length() > 0) { - msg += " Used "; - for (uint i = 0; i < materials.length(); i++) { - if (i > 0) { - if (i == materials.length() - 1) { - msg += " and "; - } else { - msg += ", "; - } - } - msg += materials[i]; - } - msg += "."; - } - + dictionary repairArgs; + repairArgs.set("vines", vines_used); + repairArgs.set("skins", skins_used); + repairArgs.set("down", down_used); + string msg = trf("system.base.resident_clothing_repair_many", repairArgs); + if (repairs_done == 1) + msg = trf("system.base.resident_clothing_repair_one", repairArgs); speak_with_history(msg, true); } } @@ -693,35 +737,45 @@ void attempt_resident_clothing_repairs() { // Resident snare retrieval void attempt_resident_snare_retrieval() { // Only during daytime - if (!is_daytime) return; + if (!is_daytime) + return; // Need residents - if (residents_count <= 0) return; + if (residents_count <= 0) + return; // Need food in storage (same limitation as other resident tasks) - if (!has_any_storage_food()) return; + if (!has_any_storage_food()) + return; int check_chance = get_resident_success_chance(RESIDENT_SNARE_CHECK_CHANCE); int escape_chance = get_resident_escape_chance(RESIDENT_SNARE_ESCAPE_CHANCE); // Check each snare that has a catch for (int i = int(world_snares.length()) - 1; i >= 0; i--) { - WorldSnare@ snare = world_snares[i]; - if (!snare.has_catch) continue; - if (!snare.active) continue; + WorldSnare @snare = world_snares[i]; + if (!snare.has_catch) + continue; + if (!snare.active) + continue; // Each snare has a chance to be checked by a resident this hour - if (random(1, 100) > check_chance) continue; + if (random(1, 100) > check_chance) + continue; // Small chance the game escapes during retrieval (like normal) if (random(1, 100) <= escape_chance) { - notify("A " + snare.catch_type + " escaped while a resident checked the snare at x " + snare.position + "."); + dictionary escapedArgs; + escapedArgs.set("catch_type", i18n_translate_fragment_value(snare.catch_type)); + escapedArgs.set("position", snare.position); + notify(trf("system.base.snare_escape_during_check", escapedArgs)); remove_snare_at(snare.position); continue; } // Check if storage has room for small game - if (get_storage_count(ITEM_SMALL_GAME) >= get_storage_stack_limit()) continue; + if (get_storage_count(ITEM_SMALL_GAME) >= get_storage_stack_limit()) + continue; // Retrieve the game string game_type = snare.catch_type; @@ -739,33 +793,43 @@ void attempt_resident_snare_retrieval() { snare.hours_with_catch = 0; snare.hour_timer.restart(); - notify("Resident retrieved " + game_type + " from snare at x " + pos + " y 0 and reset it."); + dictionary snareRetrievedArgs; + snareRetrievedArgs.set("game_type", i18n_translate_fragment_value(game_type)); + snareRetrievedArgs.set("position", pos); + notify(trf("system.base.resident_retrieved_from_snare", snareRetrievedArgs)); } } // Resident butchering - processes up to residents_count games per day (doubled when blessed) void attempt_resident_butchering() { // Need residents - if (residents_count <= 0) return; + if (residents_count <= 0) + return; // Need food in storage (same limitation as other resident tasks) - if (!has_any_storage_food()) return; + if (!has_any_storage_food()) + return; // Need game in storage - if (get_storage_count(ITEM_SMALL_GAME) <= 0 && get_storage_count(ITEM_BOAR_CARCASSES) <= 0) return; + if (get_storage_count(ITEM_SMALL_GAME) <= 0 && get_storage_count(ITEM_BOAR_CARCASSES) <= 0) + return; // Need a knife in storage - if (get_storage_count(ITEM_KNIVES) <= 0) return; + if (get_storage_count(ITEM_KNIVES) <= 0) + return; // Need a fire in base - if (!has_burning_fire_in_base()) return; + if (!has_burning_fire_in_base()) + return; int attempts = residents_count * get_resident_effect_multiplier(); int break_chance = get_resident_break_chance(RESIDENT_TOOL_BREAK_CHANCE); for (int attempt = 0; attempt < attempts; attempt++) { // Need game in storage - if (get_storage_count(ITEM_SMALL_GAME) <= 0 && get_storage_count(ITEM_BOAR_CARCASSES) <= 0) return; - if (get_storage_count(ITEM_KNIVES) <= 0) return; + if (get_storage_count(ITEM_SMALL_GAME) <= 0 && get_storage_count(ITEM_BOAR_CARCASSES) <= 0) + return; + if (get_storage_count(ITEM_KNIVES) <= 0) + return; // Determine what to butcher (prioritize boar carcasses) string game_type = ""; @@ -804,11 +868,16 @@ void attempt_resident_butchering() { } // Check storage capacity for outputs - if (meat_yield > 0 && get_storage_count(ITEM_MEAT) + meat_yield > get_storage_stack_limit()) return; - if (skins_yield > 0 && get_storage_count(ITEM_SKINS) + skins_yield > get_storage_stack_limit()) return; - if (feathers_yield > 0 && get_storage_count(ITEM_FEATHERS) + feathers_yield > get_storage_stack_limit()) return; - if (down_yield > 0 && get_storage_count(ITEM_DOWN) + down_yield > get_storage_stack_limit()) return; - if (sinew_yield > 0 && get_storage_count(ITEM_SINEW) + sinew_yield > get_storage_stack_limit()) return; + if (meat_yield > 0 && get_storage_count(ITEM_MEAT) + meat_yield > get_storage_stack_limit()) + return; + if (skins_yield > 0 && get_storage_count(ITEM_SKINS) + skins_yield > get_storage_stack_limit()) + return; + if (feathers_yield > 0 && get_storage_count(ITEM_FEATHERS) + feathers_yield > get_storage_stack_limit()) + return; + if (down_yield > 0 && get_storage_count(ITEM_DOWN) + down_yield > get_storage_stack_limit()) + return; + if (sinew_yield > 0 && get_storage_count(ITEM_SINEW) + sinew_yield > get_storage_stack_limit()) + return; // Consume the game if (is_boar) { @@ -823,38 +892,29 @@ void attempt_resident_butchering() { // Check for knife breakage if (random(1, 100) <= break_chance) { add_storage_count(ITEM_KNIVES, -1); - notify("A resident's knife broke while butchering."); + notify(tr("system.base.resident_knife_broke_butchering")); } // Add outputs to storage - if (meat_yield > 0) add_storage_count(ITEM_MEAT, meat_yield); - if (skins_yield > 0) add_storage_count(ITEM_SKINS, skins_yield); - if (feathers_yield > 0) add_storage_count(ITEM_FEATHERS, feathers_yield); - if (down_yield > 0) add_storage_count(ITEM_DOWN, down_yield); - if (sinew_yield > 0) add_storage_count(ITEM_SINEW, sinew_yield); + if (meat_yield > 0) + add_storage_count(ITEM_MEAT, meat_yield); + if (skins_yield > 0) + add_storage_count(ITEM_SKINS, skins_yield); + if (feathers_yield > 0) + add_storage_count(ITEM_FEATHERS, feathers_yield); + if (down_yield > 0) + add_storage_count(ITEM_DOWN, down_yield); + if (sinew_yield > 0) + add_storage_count(ITEM_SINEW, sinew_yield); - // Build notification message - string result = "Resident butchered " + game_type + ". Added "; - string[] outputs; - if (meat_yield > 0) outputs.insert_last(meat_yield + " meat"); - if (skins_yield > 0) outputs.insert_last(skins_yield + " skins"); - if (feathers_yield > 0) outputs.insert_last(feathers_yield + " feathers"); - if (down_yield > 0) outputs.insert_last(down_yield + " down"); - if (sinew_yield > 0) outputs.insert_last(sinew_yield + " sinew"); - - for (uint i = 0; i < outputs.length(); i++) { - if (i > 0) { - if (i == outputs.length() - 1) { - result += " and "; - } else { - result += ", "; - } - } - result += outputs[i]; - } - result += " to storage."; - - notify(result); + dictionary butcherArgs; + butcherArgs.set("game_type", i18n_translate_fragment_value(game_type)); + butcherArgs.set("meat", meat_yield); + butcherArgs.set("skins", skins_yield); + butcherArgs.set("feathers", feathers_yield); + butcherArgs.set("down", down_yield); + butcherArgs.set("sinew", sinew_yield); + notify(trf("system.base.resident_butchered_result", butcherArgs)); } } @@ -862,16 +922,21 @@ void attempt_resident_butchering() { void attempt_resident_collection() { // Only during daytime - if (!is_daytime) return; + if (!is_daytime) + return; // Need residents - if (residents_count <= 0) return; + if (residents_count <= 0) + return; // Need baskets in storage to enable collection - if (get_storage_count(ITEM_REED_BASKETS) <= 0) return; + if (get_storage_count(ITEM_REED_BASKETS) <= 0) + return; // Number of residents who can collect = min(residents, baskets) - int active_collectors = (residents_count < get_storage_count(ITEM_REED_BASKETS)) ? residents_count : get_storage_count(ITEM_REED_BASKETS); + int active_collectors = (residents_count < get_storage_count(ITEM_REED_BASKETS)) + ? residents_count + : get_storage_count(ITEM_REED_BASKETS); // Each active collector has a 10% chance to collect something int baskets_broken = 0; @@ -884,7 +949,8 @@ void attempt_resident_collection() { continue; } - if (random(1, 100) > collection_chance) continue; + if (random(1, 100) > collection_chance) + continue; // Determine what to collect (weighted random) // Sticks and vines more common, logs and stones less common @@ -911,7 +977,9 @@ void attempt_resident_collection() { // Announce only if player is in base if (x <= BASE_END) { - speak_with_history("Resident added " + item_name + " to storage.", true); + dictionary addedArgs; + addedArgs.set("item", i18n_translate_fragment_value(item_name)); + speak_with_history(trf("system.base.resident_added_to_storage", addedArgs), true); } } @@ -919,9 +987,12 @@ void attempt_resident_collection() { if (baskets_broken > 0) { add_storage_count(ITEM_REED_BASKETS, -baskets_broken); if (x <= BASE_END) { - string msg = (baskets_broken == 1) - ? "A resident's basket broke." - : baskets_broken + " baskets broke."; + string msg = tr("system.base.resident_basket_broke_one"); + if (baskets_broken > 1) { + dictionary basketBrokenArgs; + basketBrokenArgs.set("count", baskets_broken); + msg = trf("system.base.resident_basket_broke_many", basketBrokenArgs); + } speak_with_history(msg, true); } } @@ -930,19 +1001,25 @@ void attempt_resident_collection() { // Resident foraging - produces baskets of fruits and nuts from reed baskets void attempt_resident_foraging() { // Only during daytime - if (!is_daytime) return; + if (!is_daytime) + return; // Need residents - if (residents_count <= 0) return; + if (residents_count <= 0) + return; // Need reed baskets in storage - if (get_storage_count(ITEM_REED_BASKETS) <= 0) return; + if (get_storage_count(ITEM_REED_BASKETS) <= 0) + return; // Check if storage has room for basket food - if (get_storage_count(ITEM_BASKET_FOOD) >= get_storage_stack_limit()) return; + if (get_storage_count(ITEM_BASKET_FOOD) >= get_storage_stack_limit()) + return; // Number of residents who can forage = min(residents, baskets) - int active_foragers = (residents_count < get_storage_count(ITEM_REED_BASKETS)) ? residents_count : get_storage_count(ITEM_REED_BASKETS); + int active_foragers = (residents_count < get_storage_count(ITEM_REED_BASKETS)) + ? residents_count + : get_storage_count(ITEM_REED_BASKETS); int baskets_produced = 0; int baskets_broken = 0; @@ -957,10 +1034,12 @@ void attempt_resident_foraging() { } // Check if foraging succeeds - if (random(1, 100) > forage_chance) continue; + if (random(1, 100) > forage_chance) + continue; // Check storage capacity - if (get_storage_count(ITEM_BASKET_FOOD) >= get_storage_stack_limit()) break; + if (get_storage_count(ITEM_BASKET_FOOD) >= get_storage_stack_limit()) + break; // Consume a reed basket and produce a basket of fruits and nuts add_storage_count(ITEM_REED_BASKETS, -1); @@ -972,9 +1051,12 @@ void attempt_resident_foraging() { if (baskets_broken > 0) { add_storage_count(ITEM_REED_BASKETS, -baskets_broken); if (x <= BASE_END) { - string msg = (baskets_broken == 1) - ? "A resident's basket broke while foraging." - : baskets_broken + " baskets broke while foraging."; + string msg = tr("system.base.resident_basket_broke_foraging_one"); + if (baskets_broken > 1) { + dictionary basketForagingArgs; + basketForagingArgs.set("count", baskets_broken); + msg = trf("system.base.resident_basket_broke_foraging_many", basketForagingArgs); + } speak_with_history(msg, true); } } @@ -982,9 +1064,11 @@ void attempt_resident_foraging() { // Notify of production if (baskets_produced > 0 && x <= BASE_END) { if (baskets_produced == 1) { - speak_with_history("Resident gathered a basket of fruits and nuts.", true); + speak_with_history(tr("system.base.resident_gathered_basket_food_one"), true); } else { - speak_with_history("Residents gathered " + baskets_produced + " baskets of fruits and nuts.", true); + dictionary basketFoodArgs; + basketFoodArgs.set("count", baskets_produced); + speak_with_history(trf("system.base.resident_gathered_basket_food_many", basketFoodArgs), true); } } } diff --git a/src/bosses/adventure_combat.nvgt b/src/bosses/adventure_combat.nvgt index 219792d..f4c289b 100644 --- a/src/bosses/adventure_combat.nvgt +++ b/src/bosses/adventure_combat.nvgt @@ -18,7 +18,7 @@ void reset_adventure_combat_state() { last_sling_stage = -1; } -void update_weapon_range_audio_with_listener(int listener_x, int creature_pos, bool &inout was_in_range) { +void update_weapon_range_audio_with_listener(int listener_x, int creature_pos, bool& inout was_in_range) { int range = get_current_weapon_range(); bool in_range = (range >= 0) && (abs(creature_pos - listener_x) <= range); if (in_range && !was_in_range) { @@ -37,24 +37,23 @@ void adventure_start_bow_shot_audio(int listener_x, int start_x, int end_x, int bow_shot_end_x = end_x; bow_shot_hit_x = hit_x; bow_shot_duration_ms = duration_ms; - if (bow_shot_duration_ms < 1) bow_shot_duration_ms = 1; + if (bow_shot_duration_ms < 1) + bow_shot_duration_ms = 1; - bow_shot_sound_handle = play_1d_with_volume_step( - "sounds/weapons/arrow_flies.ogg", - listener_x, - bow_shot_start_x, - false, - PLAYER_WEAPON_SOUND_VOLUME_STEP - ); + bow_shot_sound_handle = play_1d_with_volume_step("sounds/weapons/arrow_flies.ogg", listener_x, bow_shot_start_x, + false, PLAYER_WEAPON_SOUND_VOLUME_STEP); } void adventure_update_bow_shot(int listener_x) { - if (!bow_shot_active) return; - if (bow_shot_duration_ms < 1) bow_shot_duration_ms = 1; + if (!bow_shot_active) + return; + if (bow_shot_duration_ms < 1) + bow_shot_duration_ms = 1; int elapsed = bow_shot_timer.elapsed; float progress = float(elapsed) / float(bow_shot_duration_ms); - if (progress > 1.0f) progress = 1.0f; + if (progress > 1.0f) + progress = 1.0f; int travel = int(float(bow_shot_end_x - bow_shot_start_x) * progress); int current_pos = bow_shot_start_x + travel; @@ -68,13 +67,8 @@ void adventure_update_bow_shot(int listener_x) { stop_bow_shot_audio(); adventure_arrow_recover_pending = false; if (hit_x >= 0) { - play_1d_with_volume_step( - "sounds/weapons/arrow_hit.ogg", - listener_x, - hit_x, - false, - PLAYER_WEAPON_SOUND_VOLUME_STEP - ); + play_1d_with_volume_step("sounds/weapons/arrow_hit.ogg", listener_x, hit_x, false, + PLAYER_WEAPON_SOUND_VOLUME_STEP); } if (recover_pending) { add_personal_count(ITEM_ARROWS, 1); @@ -82,7 +76,7 @@ void adventure_update_bow_shot(int listener_x) { } } -void adventure_release_bow_attack(int player_x, int player_facing, AdventureRangedReleaseCallback@ ranged_callback) { +void adventure_release_bow_attack(int player_x, int player_facing, AdventureRangedReleaseCallback @ranged_callback) { if (get_personal_count(ITEM_ARROWS) <= 0) { speak_ammo_blocked("No arrows."); return; @@ -99,23 +93,23 @@ void adventure_release_bow_attack(int player_x, int player_facing, AdventureRang target_x = ranged_callback(player_x, search_direction, BOW_RANGE, ADVENTURE_WEAPON_BOW, damage); } - int end_x = (target_x != -1) - ? target_x - : (player_x + (search_direction * (BOW_RANGE + BOW_MISS_EXTRA_TILES))); + int end_x = (target_x != -1) ? target_x : (player_x + (search_direction * (BOW_RANGE + BOW_MISS_EXTRA_TILES))); int duration_ms = ARROW_FLIES_DURATION_MS; if (target_x != -1) { int distance = abs(target_x - player_x); - if (distance < 1) distance = 1; + if (distance < 1) + distance = 1; duration_ms = int(float(ARROW_FLIES_DURATION_MS) * (float(distance) / float(BOW_RANGE))); - if (duration_ms < 1) duration_ms = 1; + if (duration_ms < 1) + duration_ms = 1; } adventure_arrow_recover_pending = (random(1, 100) <= 25); adventure_start_bow_shot_audio(player_x, player_x, end_x, target_x, duration_ms); } -void adventure_release_sling_attack(int player_x, int player_facing, AdventureRangedReleaseCallback@ ranged_callback) { +void adventure_release_sling_attack(int player_x, int player_facing, AdventureRangedReleaseCallback @ranged_callback) { add_personal_count(ITEM_STONES, -1); int elapsed = sling_charge_timer.elapsed; @@ -124,7 +118,6 @@ void adventure_release_sling_attack(int player_x, int player_facing, AdventureRa int stage = time_in_cycle / 500; // 0=low, 1=in-range, 2=high if (stage != 1) { - speak_with_history("Stone missed.", true); return; } @@ -138,9 +131,9 @@ void adventure_release_sling_attack(int player_x, int player_facing, AdventureRa } if (target_x == -1) { - speak_with_history("Stone missed.", true); return; } - play_1d_with_volume_step("sounds/weapons/sling_hit.ogg", player_x, target_x, false, PLAYER_WEAPON_SOUND_VOLUME_STEP); + play_1d_with_volume_step("sounds/weapons/sling_hit.ogg", player_x, target_x, false, + PLAYER_WEAPON_SOUND_VOLUME_STEP); } diff --git a/src/bosses/adventure_system.nvgt b/src/bosses/adventure_system.nvgt index 3432d67..1642405 100644 --- a/src/bosses/adventure_system.nvgt +++ b/src/bosses/adventure_system.nvgt @@ -13,66 +13,68 @@ void check_adventure_menu(int player_x) { void run_adventure_menu(int player_x) { if (player_x <= BASE_END) { - speak_with_history("No adventures available in the base.", true); + speak_with_history(tr("system.adventure.none_in_base"), true); return; } if (last_adventure_day == current_day) { - speak_with_history("You have already attempted an adventure today.", true); + speak_with_history(tr("system.adventure.already_attempted_today"), true); return; } string terrain = get_terrain_at_position(player_x); - MountainRange@ mountain = get_mountain_at(player_x); - + MountainRange @mountain = get_mountain_at(player_x); + // Check available adventures based on terrain string[] options; int[] adventure_ids; // 1 = Unicorn, 2 = Bandit's Hideout - + if (mountain !is null) { // Mountain terrain - options.insert_last("Unicorn Hunt (Mountain Boss)"); + options.insert_last(tr("system.adventure.option.unicorn_hunt")); adventure_ids.insert_last(1); } if (mountain is null && (terrain == "forest" || terrain == "deep_forest")) { - options.insert_last("Bandit's Hideout"); + options.insert_last(tr("system.adventure.option.bandit_hideout")); adventure_ids.insert_last(ADVENTURE_BANDIT_HIDEOUT); } - + if (options.length() == 0) { - speak_with_history("No adventures found in this area.", true); + speak_with_history(tr("system.adventure.none_in_area"), true); return; } - + // Show Menu - speak_with_history("Adventure Menu.", true); + speak_with_history(tr("system.adventure.menu.prompt"), true); int selection = 0; speak_with_history(options[selection], true); - + while (true) { wait(5); handle_global_volume_keys(); - + if (key_pressed(KEY_ESCAPE)) { - speak_with_history("Closed.", true); + speak_with_history(tr("system.menu.closed"), true); return; } - + 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); } - + if (key_pressed(KEY_RETURN)) { play_menu_select_sound(); start_adventure(adventure_ids[selection]); diff --git a/src/bosses/bandit_hideout.nvgt b/src/bosses/bandit_hideout.nvgt index be26606..f2ca65c 100644 --- a/src/bosses/bandit_hideout.nvgt +++ b/src/bosses/bandit_hideout.nvgt @@ -27,7 +27,7 @@ class HideoutBandit { timer attackTimer; int moveInterval; - HideoutBandit(int pos, const string&in alert, const string&in weapon, int interval) { + HideoutBandit(int pos, const string& in alert, const string& in weapon, int interval) { position = pos; health = BANDIT_HEALTH; alertSound = alert; @@ -40,7 +40,7 @@ class HideoutBandit { } } -HideoutBandit@[] hideoutBandits; +HideoutBandit @[] hideoutBandits; string[] hideoutTerrain; int hideoutPlayerX = 0; int hideoutPlayerFacing = 1; // 0 = west, 1 = east @@ -74,8 +74,10 @@ void restart_hideout_adventure_timers() { string pick_hideout_terrain() { int roll = random(0, 2); - if (roll == 0) return "grass"; - if (roll == 1) return "gravel"; + if (roll == 0) + return "grass"; + if (roll == 1) + return "gravel"; return "stone"; } @@ -96,30 +98,35 @@ void build_hideout_terrain() { } string get_hideout_terrain_at(int pos) { - if (pos < 0 || pos >= int(hideoutTerrain.length())) return "grass"; + if (pos < 0 || pos >= int(hideoutTerrain.length())) + return "grass"; string terrain = hideoutTerrain[pos]; - if (terrain == "") return "grass"; + if (terrain == "") + return "grass"; return terrain; } string get_hideout_footstep_sound(int pos) { string terrain = get_hideout_terrain_at(pos); - if (terrain == "stone") return "sounds/terrain/stone.ogg"; - if (terrain == "gravel") return "sounds/terrain/gravel.ogg"; + if (terrain == "stone") + return "sounds/terrain/stone.ogg"; + if (terrain == "gravel") + return "sounds/terrain/gravel.ogg"; return "sounds/terrain/grass.ogg"; } void play_hideout_player_footstep() { string soundFile = get_hideout_footstep_sound(hideoutPlayerX); - if (file_exists(soundFile)) { + if (audio_asset_exists(soundFile)) { p.play_stationary(soundFile, false); } } void play_hideout_positional_footstep(int listenerX, int stepX, int maxDistance, float volumeStep) { - if (abs(stepX - listenerX) > maxDistance) return; + if (abs(stepX - listenerX) > maxDistance) + return; string soundFile = get_hideout_footstep_sound(stepX); - if (file_exists(soundFile)) { + if (audio_asset_exists(soundFile)) { play_1d_with_volume_step(soundFile, listenerX, stepX, false, volumeStep); } } @@ -135,7 +142,7 @@ void clear_hideout_bandits() { hideoutBandits.resize(0); } -HideoutBandit@ get_hideout_bandit_at(int pos) { +HideoutBandit @get_hideout_bandit_at(int pos) { for (uint i = 0; i < hideoutBandits.length(); i++) { if (hideoutBandits[i].position == pos) { return @hideoutBandits[i]; @@ -144,7 +151,8 @@ HideoutBandit@ get_hideout_bandit_at(int pos) { return null; } -bool pet_find_hideout_target(int originPos, int referencePos, int &out targetPos, string &out targetLabel, int &out targetKind) { +bool pet_find_hideout_target(int originPos, int referencePos, int& out targetPos, string& out targetLabel, + int& out targetKind) { int bestDistance = PET_RANGE + 1; targetPos = -1; targetLabel = ""; @@ -152,7 +160,8 @@ bool pet_find_hideout_target(int originPos, int referencePos, int &out targetPos for (uint i = 0; i < hideoutBandits.length(); i++) { int distanceToOrigin = abs(hideoutBandits[i].position - originPos); - if (distanceToOrigin > PET_RANGE) continue; + if (distanceToOrigin > PET_RANGE) + continue; int distance = abs(hideoutBandits[i].position - referencePos); if (distance < bestDistance) { bestDistance = distance; @@ -166,19 +175,24 @@ bool pet_find_hideout_target(int originPos, int referencePos, int &out targetPos } bool pet_damage_hideout_target(int targetKind, int targetPos, int damage) { - if (targetKind != 0) return false; + if (targetKind != 0) + return false; return damage_hideout_bandit_at(targetPos, damage); } int clamp_hideout_spawn_start(int startX) { - if (startX < 0) return 0; - if (startX >= BANDIT_HIDEOUT_MAP_SIZE) return BANDIT_HIDEOUT_MAP_SIZE - 1; + if (startX < 0) + return 0; + if (startX >= BANDIT_HIDEOUT_MAP_SIZE) + return BANDIT_HIDEOUT_MAP_SIZE - 1; return startX; } int clamp_hideout_spawn_end(int endX) { - if (endX < 0) return 0; - if (endX >= BANDIT_HIDEOUT_MAP_SIZE) return BANDIT_HIDEOUT_MAP_SIZE - 1; + if (endX < 0) + return 0; + if (endX >= BANDIT_HIDEOUT_MAP_SIZE) + return BANDIT_HIDEOUT_MAP_SIZE - 1; return endX; } @@ -194,8 +208,10 @@ int pick_hideout_spawn_position(int startX, int endX) { int spawnX = -1; for (int attempt = 0; attempt < 20; attempt++) { int candidate = random(startClamp, endClamp); - if (candidate == hideoutPlayerX) continue; - if (get_hideout_bandit_at(candidate) != null) continue; + if (candidate == hideoutPlayerX) + continue; + if (get_hideout_bandit_at(candidate) != null) + continue; spawnX = candidate; break; } @@ -208,13 +224,15 @@ int pick_hideout_spawn_position(int startX, int endX) { void spawn_hideout_bandit_in_range(int startX, int endX) { int spawnX = pick_hideout_spawn_position(startX, endX); string alertSound = pick_invader_alert_sound("bandit"); - if (alertSound == "") alertSound = "sounds/enemies/bandit1.ogg"; + if (alertSound == "") + alertSound = "sounds/enemies/bandit1.ogg"; string weaponType = (random(0, 1) == 0) ? "spear" : "axe"; int moveInterval = random(BANDIT_MOVE_INTERVAL_MIN, BANDIT_MOVE_INTERVAL_MAX); - HideoutBandit@ bandit = HideoutBandit(spawnX, alertSound, weaponType, moveInterval); + HideoutBandit @bandit = HideoutBandit(spawnX, alertSound, weaponType, moveInterval); hideoutBandits.insert_last(bandit); - bandit.soundHandle = play_1d_with_volume_step(bandit.alertSound, hideoutPlayerX, bandit.position, true, BANDIT_SOUND_VOLUME_STEP); + bandit.soundHandle = + play_1d_with_volume_step(bandit.alertSound, hideoutPlayerX, bandit.position, true, BANDIT_SOUND_VOLUME_STEP); } void spawn_hideout_bandits_initial() { @@ -228,7 +246,8 @@ void spawn_hideout_bandits_initial() { void respawn_hideout_bandit() { int startSpawnStart = 0; int startSpawnEnd = BANDIT_HIDEOUT_START_SPAWN_RANGE - 1; - if (startSpawnEnd > hideoutBaseX) startSpawnEnd = hideoutBaseX; + if (startSpawnEnd > hideoutBaseX) + startSpawnEnd = hideoutBaseX; int baseSpawnStart = hideoutBaseX - (BANDIT_HIDEOUT_BASE_SPAWN_RANGE - 1); int baseSpawnEnd = hideoutBaseX; @@ -250,8 +269,10 @@ void init_bandit_hideout_adventure() { hideoutPlayerJumping = false; int barricadeBase = current_day * BANDIT_HIDEOUT_BARRICADE_HP_PER_DAY; - if (barricadeBase < BANDIT_HIDEOUT_BARRICADE_HP_PER_DAY) barricadeBase = BANDIT_HIDEOUT_BARRICADE_HP_PER_DAY; - if (barricadeBase > BANDIT_HIDEOUT_BARRICADE_HP_MAX) barricadeBase = BANDIT_HIDEOUT_BARRICADE_HP_MAX; + if (barricadeBase < BANDIT_HIDEOUT_BARRICADE_HP_PER_DAY) + barricadeBase = BANDIT_HIDEOUT_BARRICADE_HP_PER_DAY; + if (barricadeBase > BANDIT_HIDEOUT_BARRICADE_HP_MAX) + barricadeBase = BANDIT_HIDEOUT_BARRICADE_HP_MAX; hideoutBarricadeMax = barricadeBase; hideoutBarricadeHealth = barricadeBase; @@ -281,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); @@ -297,12 +319,12 @@ void run_bandit_hideout_adventure() { adventurePaused = !adventurePaused; if (adventurePaused) { p.pause_all(); - speak_with_history("Paused. Press backspace to resume.", true); + speak_with_history(tr("system.game.paused_prompt"), true); } else { p.resume_all(); restart_hideout_adventure_timers(); restart_all_timers(); - speak_with_history("Resumed.", true); + speak_with_history(tr("system.game.resumed"), true); } continue; } @@ -315,7 +337,7 @@ void run_bandit_hideout_adventure() { if (key_pressed(KEY_ESCAPE)) { cleanup_bandit_hideout_adventure(); - speak_with_history("You flee the hideout.", true); + speak_with_history(tr("system.adventure.bandit_hideout.flee"), true); return; } @@ -331,9 +353,17 @@ void run_bandit_hideout_adventure() { if (key_pressed(KEY_X)) { int distanceToBase = hideoutBaseX - hideoutPlayerX; - if (distanceToBase < 0) distanceToBase = 0; - string terrain = get_hideout_terrain_at(hideoutPlayerX); - speak_with_history("x " + hideoutPlayerX + ", terrain " + terrain + ". Base " + distanceToBase + " tiles east. Barricade " + hideoutBarricadeHealth + " of " + hideoutBarricadeMax + ".", true); + if (distanceToBase < 0) + distanceToBase = 0; + string terrain = i18n_translate_fragment_value(get_hideout_terrain_at(hideoutPlayerX)); + dictionary reportArgs; + reportArgs.set("x", hideoutPlayerX); + reportArgs.set("terrain", terrain); + reportArgs.set("distance", distanceToBase); + reportArgs.set("direction", tr("system.direction.east")); + reportArgs.set("health", hideoutBarricadeHealth); + reportArgs.set("max", hideoutBarricadeMax); + speak_with_history(trf("system.adventure.bandit_hideout.position_report", reportArgs), true); } handle_hideout_player_movement(); @@ -355,7 +385,7 @@ void run_bandit_hideout_adventure() { if (player_health <= 0) { cleanup_bandit_hideout_adventure(); - speak_with_history("The bandits cut you down.", true); + speak_with_history(tr("system.adventure.bandit_hideout.player_defeated"), true); return; } @@ -366,12 +396,12 @@ void run_bandit_hideout_adventure() { void handle_hideout_player_movement() { if (key_pressed(KEY_LEFT) && hideoutPlayerFacing != 0) { hideoutPlayerFacing = 0; - speak_with_history("west", true); + speak_with_history(tr("system.direction.west"), true); hideoutWalkTimer.restart(); } if (key_pressed(KEY_RIGHT) && hideoutPlayerFacing != 1) { hideoutPlayerFacing = 1; - speak_with_history("east", true); + speak_with_history(tr("system.direction.east"), true); hideoutWalkTimer.restart(); } @@ -381,12 +411,14 @@ void handle_hideout_player_movement() { hideoutPlayerFacing = 0; hideoutPlayerX--; hideoutWalkTimer.restart(); - if (player_health > 0 && !hideoutPlayerJumping) play_hideout_player_footstep(); + if (player_health > 0 && !hideoutPlayerJumping) + play_hideout_player_footstep(); } else if (key_down(KEY_RIGHT) && hideoutPlayerX < hideoutBaseX) { hideoutPlayerFacing = 1; hideoutPlayerX++; hideoutWalkTimer.restart(); - if (player_health > 0 && !hideoutPlayerJumping) play_hideout_player_footstep(); + if (player_health > 0 && !hideoutPlayerJumping) + play_hideout_player_footstep(); } } @@ -444,13 +476,17 @@ void handle_hideout_player_actions() { } if (!bow_equipped && !bow_drawing && !sling_equipped && !sling_charging) { - if (fishing_pole_equipped) return; + if (fishing_pole_equipped) + return; int weaponType = get_hideout_melee_weapon_type(); - if (weaponType == -1) return; + if (weaponType == -1) + return; int attackCooldown = 1000; - if (weaponType == ADVENTURE_WEAPON_SPEAR) attackCooldown = 800; - if (weaponType == ADVENTURE_WEAPON_AXE) attackCooldown = 1600; + if (weaponType == ADVENTURE_WEAPON_SPEAR) + attackCooldown = 800; + if (weaponType == ADVENTURE_WEAPON_AXE) + attackCooldown = 1600; if (ctrlDown && hideoutAttackTimer.elapsed > attackCooldown) { hideoutAttackTimer.restart(); @@ -491,7 +527,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; } @@ -502,12 +538,14 @@ 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() { - if (spear_equipped) return ADVENTURE_WEAPON_SPEAR; - if (axe_equipped) return ADVENTURE_WEAPON_AXE; + if (spear_equipped) + return ADVENTURE_WEAPON_SPEAR; + if (axe_equipped) + return ADVENTURE_WEAPON_AXE; return -1; } @@ -550,18 +588,23 @@ bool hideout_melee_hit(int weaponType) { } bool apply_hideout_barricade_damage(int damage) { - if (damage <= 0) return false; - if (hideoutBarricadeHealth <= 0) return false; + if (damage <= 0) + return false; + if (hideoutBarricadeHealth <= 0) + return false; hideoutBarricadeHealth -= damage; - if (hideoutBarricadeHealth < 0) hideoutBarricadeHealth = 0; - play_1d_with_volume_step("sounds/weapons/axe_hit.ogg", hideoutPlayerX, hideoutBaseX, false, BANDIT_SOUND_VOLUME_STEP); + if (hideoutBarricadeHealth < 0) + hideoutBarricadeHealth = 0; + play_1d_with_volume_step("sounds/weapons/axe_hit.ogg", hideoutPlayerX, hideoutBaseX, false, + BANDIT_SOUND_VOLUME_STEP); return true; } int find_hideout_ranged_target(int playerX, int direction, int range) { for (int dist = 1; dist <= range; dist++) { int checkX = playerX + (dist * direction); - if (checkX < 0 || checkX >= BANDIT_HIDEOUT_MAP_SIZE) break; + if (checkX < 0 || checkX >= BANDIT_HIDEOUT_MAP_SIZE) + break; if (get_hideout_bandit_at(checkX) != null) { return checkX; @@ -576,7 +619,8 @@ int find_hideout_ranged_target(int playerX, int direction, int range) { int bandit_hideout_ranged_attack(int playerX, int direction, int range, int weaponType, int damage) { int targetX = find_hideout_ranged_target(playerX, direction, range); - if (targetX == -1) return -1; + if (targetX == -1) + return -1; if (targetX == hideoutBaseX) { apply_hideout_barricade_damage(damage); @@ -602,7 +646,8 @@ bool damage_hideout_bandit_at(int pos, int damage) { if (hideoutBandits[i].inWeaponRange) { play_weapon_range_sound("sounds/enemies/exit_range.ogg", hideoutBandits[i].position); } - play_creature_death_sounds("sounds/enemies/enemy_falls.ogg", hideoutBandits[i].alertSound, hideoutPlayerX, pos, BANDIT_SOUND_VOLUME_STEP); + play_creature_death_sounds("sounds/enemies/enemy_falls.ogg", hideoutBandits[i].alertSound, + hideoutPlayerX, pos, BANDIT_SOUND_VOLUME_STEP); hideoutBandits.remove_at(i); hideoutBanditsKilled++; respawn_hideout_bandit(); @@ -616,37 +661,46 @@ bool damage_hideout_bandit_at(int pos, int damage) { void update_hideout_player_jump() { if (hideoutPlayerJumping && hideoutJumpTimer.elapsed > 850) { hideoutPlayerJumping = false; - if (player_health > 0) play_hideout_player_footstep(); + if (player_health > 0) + play_hideout_player_footstep(); } } -bool try_hideout_bandit_attack_player(HideoutBandit@ bandit) { - if (player_health <= 0) return false; - if (abs(bandit.position - hideoutPlayerX) > 1) return false; - if (bandit.attackTimer.elapsed < BANDIT_ATTACK_INTERVAL) return false; +bool try_hideout_bandit_attack_player(HideoutBandit @bandit) { + if (player_health <= 0) + return false; + if (abs(bandit.position - hideoutPlayerX) > 1) + return false; + if (bandit.attackTimer.elapsed < BANDIT_ATTACK_INTERVAL) + return false; bandit.attackTimer.restart(); if (bandit.weaponType == "spear") { - play_creature_attack_sound("sounds/weapons/spear_swing.ogg", hideoutPlayerX, bandit.position, BANDIT_SOUND_VOLUME_STEP); + play_creature_attack_sound("sounds/weapons/spear_swing.ogg", hideoutPlayerX, bandit.position, + BANDIT_SOUND_VOLUME_STEP); } else { - play_creature_attack_sound("sounds/weapons/axe_swing.ogg", hideoutPlayerX, bandit.position, BANDIT_SOUND_VOLUME_STEP); + play_creature_attack_sound("sounds/weapons/axe_swing.ogg", hideoutPlayerX, bandit.position, + BANDIT_SOUND_VOLUME_STEP); } int damage = random(BANDIT_DAMAGE_MIN, BANDIT_DAMAGE_MAX); player_health -= damage; - if (player_health < 0) player_health = 0; + if (player_health < 0) + player_health = 0; if (bandit.weaponType == "spear") { - play_creature_attack_sound("sounds/weapons/spear_hit.ogg", hideoutPlayerX, bandit.position, BANDIT_SOUND_VOLUME_STEP); + play_creature_attack_sound("sounds/weapons/spear_hit.ogg", hideoutPlayerX, bandit.position, + BANDIT_SOUND_VOLUME_STEP); } else { - play_creature_attack_sound("sounds/weapons/axe_hit.ogg", hideoutPlayerX, bandit.position, BANDIT_SOUND_VOLUME_STEP); + play_creature_attack_sound("sounds/weapons/axe_hit.ogg", hideoutPlayerX, bandit.position, + BANDIT_SOUND_VOLUME_STEP); } play_player_damage_sound(); return true; } -void update_hideout_bandit_audio(HideoutBandit@ bandit) { +void update_hideout_bandit_audio(HideoutBandit @bandit) { if (bandit.soundHandle != -1 && p.sound_is_active(bandit.soundHandle)) { p.update_sound_1d(bandit.soundHandle, bandit.position); return; @@ -654,10 +708,11 @@ void update_hideout_bandit_audio(HideoutBandit@ bandit) { if (bandit.soundHandle != -1) { p.destroy_sound(bandit.soundHandle); } - bandit.soundHandle = play_1d_with_volume_step(bandit.alertSound, hideoutPlayerX, bandit.position, true, BANDIT_SOUND_VOLUME_STEP); + bandit.soundHandle = + play_1d_with_volume_step(bandit.alertSound, hideoutPlayerX, bandit.position, true, BANDIT_SOUND_VOLUME_STEP); } -void update_hideout_bandit(HideoutBandit@ bandit) { +void update_hideout_bandit(HideoutBandit @bandit) { update_weapon_range_audio_with_listener(hideoutPlayerX, bandit.position, bandit.inWeaponRange); if (try_hideout_bandit_attack_player(bandit)) { @@ -672,15 +727,18 @@ void update_hideout_bandit(HideoutBandit@ bandit) { bandit.moveTimer.restart(); int direction = 0; - if (hideoutPlayerX > bandit.position) direction = 1; - else if (hideoutPlayerX < bandit.position) direction = -1; + if (hideoutPlayerX > bandit.position) + direction = 1; + else if (hideoutPlayerX < bandit.position) + direction = -1; if (direction != 0) { int targetX = bandit.position + direction; if (targetX >= 0 && targetX < BANDIT_HIDEOUT_MAP_SIZE) { if (get_hideout_bandit_at(targetX) == null) { bandit.position = targetX; - play_hideout_positional_footstep(hideoutPlayerX, bandit.position, BANDIT_FOOTSTEP_MAX_DISTANCE, BANDIT_SOUND_VOLUME_STEP); + play_hideout_positional_footstep(hideoutPlayerX, bandit.position, BANDIT_FOOTSTEP_MAX_DISTANCE, + BANDIT_SOUND_VOLUME_STEP); } } } @@ -695,18 +753,22 @@ void update_hideout_bandits() { } int add_hideout_storage_item(int itemType, int amount) { - if (amount <= 0) return 0; + if (amount <= 0) + return 0; int capacity = get_storage_stack_limit() - get_storage_count(itemType); - if (capacity <= 0) return 0; + if (capacity <= 0) + return 0; int addedAmount = amount; - if (addedAmount > capacity) addedAmount = capacity; - if (addedAmount <= 0) return 0; + if (addedAmount > capacity) + addedAmount = capacity; + if (addedAmount <= 0) + return 0; add_storage_count(itemType, addedAmount); 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) { @@ -719,7 +781,7 @@ int add_hideout_storage_item(int itemType, int amount) { } void give_bandit_hideout_rewards() { - speak_with_history("Victory!", true); + speak_with_history(tr("system.adventure.victory"), true); string[] rewards; rewards.insert_last("=== Victory Rewards ==="); @@ -728,8 +790,10 @@ void give_bandit_hideout_rewards() { if (world_altars.length() > 0) { double favorReward = BANDIT_HIDEOUT_BASE_FAVOR + (hideoutBanditsKilled * BANDIT_HIDEOUT_FAVOR_PER_KILL); - if (favorReward > BANDIT_HIDEOUT_FAVOR_MAX) favorReward = BANDIT_HIDEOUT_FAVOR_MAX; - if (favorReward < BANDIT_HIDEOUT_BASE_FAVOR) favorReward = BANDIT_HIDEOUT_BASE_FAVOR; + if (favorReward > BANDIT_HIDEOUT_FAVOR_MAX) + favorReward = BANDIT_HIDEOUT_FAVOR_MAX; + if (favorReward < BANDIT_HIDEOUT_BASE_FAVOR) + favorReward = BANDIT_HIDEOUT_BASE_FAVOR; favor += favorReward; rewards.insert_last("Favor awarded: " + format_favor(favorReward) + "."); } else { @@ -745,9 +809,11 @@ void give_bandit_hideout_rewards() { bool anyItems = false; for (int itemType = 0; itemType < ITEM_COUNT; itemType++) { int roll = random(0, BANDIT_HIDEOUT_ITEM_REWARD_MAX); - if (roll <= 0) continue; + if (roll <= 0) + continue; int addedAmount = add_hideout_storage_item(itemType, roll); - if (addedAmount <= 0) continue; + if (addedAmount <= 0) + continue; rewards.insert_last(get_item_display_name(itemType) + ": +" + addedAmount + "."); anyItems = true; } @@ -785,7 +851,8 @@ void give_bandit_hideout_rewards() { int survivorRoll = random(1, 100); if (survivorRoll <= 50) { residents_count += 1; - if (residents_count > MAX_RESIDENTS) residents_count = MAX_RESIDENTS; + if (residents_count > MAX_RESIDENTS) + residents_count = MAX_RESIDENTS; rewards.insert_last(""); rewards.insert_last("A survivor joins your base."); } else { @@ -794,6 +861,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 06a3e16..301e4eb 100644 --- a/src/bosses/unicorn/unicorn_boss.nvgt +++ b/src/bosses/unicorn/unicorn_boss.nvgt @@ -10,11 +10,11 @@ class UnicornBoss { timer move_timer; int sound_handle; bool on_bridge; - + UnicornBoss() { reset(); } - + void reset() { health = 450; speed = UNICORN_SPEED; @@ -32,7 +32,7 @@ const int BRIDGE_START = 45; const int BRIDGE_END = 54; const int BRIDGE_SUPPORT_MAX_HEALTH = 100; const float UNICORN_SOUND_VOLUME_STEP = 2.5; // Lower = audible from further away -const int UNICORN_SPEED = 80; // ms per tile, 100 tiles * 80ms = 8 seconds per charge +const int UNICORN_SPEED = 80; // ms per tile, 100 tiles * 80ms = 8 seconds per charge const bool UNICORN_BOW_CAN_DAMAGE_SUPPORTS = false; const bool UNICORN_SLING_CAN_DAMAGE_SUPPORTS = false; @@ -44,7 +44,7 @@ bool player_arena_jumping = false; timer arena_jump_timer; timer arena_walk_timer; timer arena_attack_timer; -int player_arena_facing = 1; // 0 = west, 1 = east +int player_arena_facing = 1; // 0 = west, 1 = east int[] bridge_supports_health; // 2 supports: Left (start) and Right (end) bool bridge_collapsed = false; string current_unicorn_sound = ""; @@ -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); @@ -131,12 +132,12 @@ void run_unicorn_adventure() { adventurePaused = !adventurePaused; if (adventurePaused) { p.pause_all(); - speak_with_history("Paused. Press backspace to resume.", true); + speak_with_history(tr("system.game.paused_prompt"), true); } else { p.resume_all(); restart_unicorn_adventure_timers(); restart_all_timers(); - speak_with_history("Resumed.", true); + speak_with_history(tr("system.game.resumed"), true); } continue; } @@ -150,7 +151,7 @@ void run_unicorn_adventure() { // Input Handling if (key_pressed(KEY_ESCAPE)) { cleanup_unicorn_adventure(); - speak_with_history("You flee the encounter.", true); + speak_with_history(tr("system.adventure.unicorn.flee"), true); return; } @@ -167,14 +168,20 @@ void run_unicorn_adventure() { // Coordinates if (key_pressed(KEY_X)) { - string facing_dir = (unicorn.facing == 1) ? "east" : "west"; - string terrain = (player_arena_x >= BRIDGE_START && player_arena_x <= BRIDGE_END && !bridge_collapsed) ? "wood" : "grass"; - speak_with_history("x " + player_arena_x + ", terrain " + terrain + ". Unicorn facing " + facing_dir, true); + string facing_dir = (unicorn.facing == 1) ? tr("system.direction.east") : tr("system.direction.west"); + string terrain = (player_arena_x >= BRIDGE_START && player_arena_x <= BRIDGE_END && !bridge_collapsed) ? + i18n_translate_fragment_value("wood") : + i18n_translate_fragment_value("grass"); + dictionary reportArgs; + reportArgs.set("x", player_arena_x); + reportArgs.set("terrain", terrain); + reportArgs.set("direction", facing_dir); + speak_with_history(trf("system.adventure.unicorn.position_report", reportArgs), true); } handle_player_movement(); handle_player_actions(); - + // Updates update_player_jump(); update_unicorn(); @@ -182,7 +189,7 @@ void run_unicorn_adventure() { update_unicorn_weapon_range_audio(); update_pet_adventure_position(player_arena_x); update_pets(); - + // Check Conditions - unicorn falls when on collapsed bridge if (!unicorn_defeated && bridge_collapsed && unicorn.x >= BRIDGE_START && unicorn.x <= BRIDGE_END) { mark_unicorn_defeated(true); @@ -202,14 +209,14 @@ void run_unicorn_adventure() { give_unicorn_rewards(); return; } - + if (player_health <= 0) { cleanup_unicorn_adventure(); - speak_with_history("The Unicorn trampled you.", true); + speak_with_history(tr("system.adventure.unicorn.player_trampled"), true); // Player death will be handled by main game loop checking player_health <= 0 - return; + return; } - + // Audio p.update_listener_1d(player_arena_x); update_unicorn_audio(); @@ -220,12 +227,12 @@ void handle_player_movement() { // Direction change announces if (key_pressed(KEY_LEFT) && player_arena_facing != 0) { player_arena_facing = 0; - speak_with_history("west", true); + speak_with_history(tr("system.direction.west"), true); arena_walk_timer.restart(); } if (key_pressed(KEY_RIGHT) && player_arena_facing != 1) { player_arena_facing = 1; - speak_with_history("east", true); + speak_with_history(tr("system.direction.east"), true); arena_walk_timer.restart(); } @@ -236,13 +243,15 @@ void handle_player_movement() { player_arena_x--; arena_walk_timer.restart(); check_player_chasm_fall(); - if (player_health > 0) play_footstep_sound(); + if (player_health > 0) + play_footstep_sound(); } else if (key_down(KEY_RIGHT) && player_arena_x < UNICORN_ARENA_SIZE - 1) { player_arena_facing = 1; player_arena_x++; arena_walk_timer.restart(); check_player_chasm_fall(); - if (player_health > 0) play_footstep_sound(); + if (player_health > 0) + play_footstep_sound(); } } @@ -283,8 +292,10 @@ void check_player_chasm_fall() { float height_remaining = float(total_fall - feet_fallen); float pitch_percent = 50.0 + (50.0 * (height_remaining / float(total_fall))); - if (pitch_percent < 50.0) pitch_percent = 50.0; - if (pitch_percent > 100.0) pitch_percent = 100.0; + if (pitch_percent < 50.0) + pitch_percent = 50.0; + if (pitch_percent > 100.0) + pitch_percent = 100.0; fall_handle = p.play_stationary_extended("sounds/actions/falling.ogg", true, 0, 0, 0, pitch_percent); } @@ -309,7 +320,8 @@ void update_player_jump() { void handle_player_actions() { // Can't attack while jumping - if (player_arena_jumping) return; + if (player_arena_jumping) + return; bool ctrl_down = (key_down(KEY_LCTRL) || key_down(KEY_RCTRL)); @@ -360,14 +372,18 @@ void handle_player_actions() { // Non-sling weapon attacks (existing pattern) if (!bow_equipped && !bow_drawing && !sling_equipped && !sling_charging) { - if (fishing_pole_equipped) return; + if (fishing_pole_equipped) + return; int weapon_type = get_unicorn_melee_weapon_type(); - if (weapon_type == -1) return; + if (weapon_type == -1) + return; int attack_cooldown = 1000; - if (weapon_type == ADVENTURE_WEAPON_SPEAR) attack_cooldown = 800; - if (weapon_type == ADVENTURE_WEAPON_AXE) attack_cooldown = 1600; + if (weapon_type == ADVENTURE_WEAPON_SPEAR) + attack_cooldown = 800; + if (weapon_type == ADVENTURE_WEAPON_AXE) + attack_cooldown = 1600; if (ctrl_down && arena_attack_timer.elapsed > attack_cooldown) { arena_attack_timer.restart(); @@ -380,8 +396,10 @@ void handle_player_actions() { } int get_unicorn_melee_weapon_type() { - if (spear_equipped) return ADVENTURE_WEAPON_SPEAR; - if (axe_equipped) return ADVENTURE_WEAPON_AXE; + if (spear_equipped) + return ADVENTURE_WEAPON_SPEAR; + if (axe_equipped) + return ADVENTURE_WEAPON_AXE; return -1; } @@ -438,7 +456,8 @@ int find_unicorn_ranged_target(int player_x, int direction, int range) { for (int dist = 1; dist <= range; dist++) { int check_x = player_x + (dist * direction); - if (check_x < 0 || check_x >= UNICORN_ARENA_SIZE) break; + if (check_x < 0 || check_x >= UNICORN_ARENA_SIZE) + break; if (bridge_supports_health[0] > 0 && check_x == BRIDGE_START) { return check_x; @@ -454,7 +473,8 @@ int find_unicorn_ranged_target(int player_x, int direction, int range) { int unicorn_ranged_attack(int player_x, int direction, int range, int weapon_type, int damage) { int target_x = find_unicorn_ranged_target(player_x, direction, range); - if (target_x == -1) return -1; + if (target_x == -1) + return -1; if (target_x == unicorn.x) { apply_unicorn_damage(damage); @@ -482,7 +502,8 @@ int unicorn_ranged_attack(int player_x, int direction, int range, int weapon_typ } void mark_unicorn_defeated(bool by_fall) { - if (unicorn_defeated) return; + if (unicorn_defeated) + return; unicorn_defeated = true; unicorn_defeated_by_fall = by_fall; if (by_fall) { @@ -491,8 +512,10 @@ void mark_unicorn_defeated(bool by_fall) { } void apply_unicorn_damage(int damage) { - if (damage <= 0) return; - if (unicorn.health <= 0) return; + if (damage <= 0) + return; + if (unicorn.health <= 0) + return; unicorn.health -= damage; if (unicorn.health <= 0) { @@ -501,12 +524,15 @@ void apply_unicorn_damage(int damage) { } } -bool pet_find_unicorn_target(int originPos, int referencePos, int &out targetPos, string &out targetLabel, int &out targetKind) { +bool pet_find_unicorn_target(int originPos, int referencePos, int& out targetPos, string& out targetLabel, + int& out targetKind) { targetPos = -1; targetLabel = ""; targetKind = -1; - if (unicorn.health <= 0) return false; - if (abs(unicorn.x - originPos) > PET_RANGE) return false; + if (unicorn.health <= 0) + return false; + if (abs(unicorn.x - originPos) > PET_RANGE) + return false; targetPos = unicorn.x; targetLabel = "unicorn"; targetKind = 0; @@ -514,7 +540,8 @@ bool pet_find_unicorn_target(int originPos, int referencePos, int &out targetPos } bool pet_damage_unicorn_target(int targetKind, int targetPos, int damage) { - if (targetKind != 0) return false; + if (targetKind != 0) + return false; apply_unicorn_damage(damage); return true; } @@ -548,7 +575,7 @@ void check_bridge_collapse() { void update_unicorn() { if (unicorn.move_timer.elapsed >= unicorn.speed) { unicorn.move_timer.restart(); - + // Move if (unicorn.facing == 1) { unicorn.x++; @@ -563,14 +590,14 @@ void update_unicorn() { unicorn.x = 0; } } - + // Bridge Logic if (unicorn.x >= BRIDGE_START && unicorn.x <= BRIDGE_END && !bridge_collapsed) { unicorn.on_bridge = true; } else { unicorn.on_bridge = false; } - + // Collision with Player if (unicorn.x == player_arena_x && player_arena_y == 0) { player_health -= 10; @@ -588,7 +615,8 @@ void update_unicorn_audio() { } // Check if we need to switch sounds (different file or no active sound) - bool need_new_sound = (unicorn.sound_handle == -1 || !p.sound_is_active(unicorn.sound_handle) || current_unicorn_sound != sound_file); + bool need_new_sound = + (unicorn.sound_handle == -1 || !p.sound_is_active(unicorn.sound_handle) || current_unicorn_sound != sound_file); if (need_new_sound) { // Stop old sound if playing @@ -596,7 +624,8 @@ void update_unicorn_audio() { p.destroy_sound(unicorn.sound_handle); } // Start new positioned sound using shared helper - unicorn.sound_handle = play_1d_with_volume_step(sound_file, player_arena_x, unicorn.x, true, UNICORN_SOUND_VOLUME_STEP); + unicorn.sound_handle = + play_1d_with_volume_step(sound_file, player_arena_x, unicorn.x, true, UNICORN_SOUND_VOLUME_STEP); current_unicorn_sound = sound_file; } else { // Update position of existing sound @@ -630,10 +659,13 @@ void play_unicorn_death_sequence() { // Pitch ranges from 100 (start) to 50 (end) like normal falling float height_remaining = float(total_fall - feet_fallen); float pitch_percent = 50.0 + (50.0 * (height_remaining / float(total_fall))); - if (pitch_percent < 50.0) pitch_percent = 50.0; - if (pitch_percent > 100.0) pitch_percent = 100.0; + if (pitch_percent < 50.0) + pitch_percent = 50.0; + if (pitch_percent > 100.0) + pitch_percent = 100.0; - fall_handle = p.play_extended_1d("sounds/actions/falling.ogg", player_arena_x, unicorn.x, 0, 0, true, 0, 0.0, 0.0, pitch_percent); + fall_handle = p.play_extended_1d("sounds/actions/falling.ogg", player_arena_x, unicorn.x, 0, 0, true, 0, + 0.0, 0.0, pitch_percent); if (fall_handle != -1) { p.update_sound_positioning_values(fall_handle, -1.0, UNICORN_SOUND_VOLUME_STEP, true); } @@ -645,12 +677,13 @@ void play_unicorn_death_sequence() { if (fall_handle != -1) { p.destroy_sound(fall_handle); } - play_1d_with_volume_step("sounds/bosses/unicorn/unicorn_falls.ogg", player_arena_x, unicorn.x, false, UNICORN_SOUND_VOLUME_STEP); + play_1d_with_volume_step("sounds/bosses/unicorn/unicorn_falls.ogg", player_arena_x, unicorn.x, false, + UNICORN_SOUND_VOLUME_STEP); wait(1500); // Let the impact sound play before cleanup } void give_unicorn_rewards() { - speak_with_history("Victory!", true); + speak_with_history(tr("system.adventure.victory"), true); // Calculate rewards double favor_reward = double(random(7, 10)) / 10.0; // 0.7-1.0 favor @@ -665,36 +698,40 @@ void give_unicorn_rewards() { // Build rewards display string[] rewards; - rewards.insert_last("=== Victory Rewards ==="); + rewards.insert_last(tr("system.adventure.rewards.title")); rewards.insert_last(""); - rewards.insert_last("The gods are pleased with your victory! " + format_favor(favor_reward) + " favor awarded."); - rewards.insert_last("Heal Scrolls: +" + scrolls_added + "."); + dictionary favorArgs; + favorArgs.set("favor", format_favor(favor_reward)); + rewards.insert_last(trf("system.adventure.unicorn.reward.favor_awarded", favorArgs)); + dictionary scrollArgs; + scrollArgs.set("count", scrolls_added); + rewards.insert_last(trf("system.adventure.unicorn.reward.heal_scrolls", scrollArgs)); rewards.insert_last(""); if (new_rune) { - rewards.insert_last("Learned Rune of Swiftness!"); - rewards.insert_last("You can now engrave equipment with this rune at the crafting menu."); + rewards.insert_last(tr("system.adventure.unicorn.reward.learned_rune_swiftness")); + rewards.insert_last(tr("system.adventure.unicorn.reward.engrave_unlocked")); } else { - rewards.insert_last("You have already mastered the Rune of Swiftness."); + rewards.insert_last(tr("system.adventure.unicorn.reward.mastered_rune_swiftness")); } rewards.insert_last(""); if (world_stables.length() == 0 || world_storages.length() == 0) { - rewards.insert_last("Stable or storage not built. No horses were captured."); + rewards.insert_last(tr("system.adventure.unicorn.horses.no_stable_or_storage")); } else if (horses_count >= MAX_HORSES) { - rewards.insert_last("Stable is full. No horses were captured."); + rewards.insert_last(tr("system.adventure.unicorn.horses.stable_full")); } else { int horseRoll = random(1, 100); if (horseRoll <= HORSE_ADVENTURE_CHANCE) { horses_count++; - rewards.insert_last("A horse joins your stable."); + rewards.insert_last(tr("system.adventure.unicorn.horses.joined_stable")); } else { - rewards.insert_last("No horses were captured."); + rewards.insert_last(tr("system.adventure.unicorn.horses.none_captured")); } } append_adventure_completion_rewards(ADVENTURE_UNICORN, rewards); // Display rewards in text reader - text_reader_lines(rewards, "Unicorn Victory", true); + text_reader_lines(rewards, tr("system.adventure.unicorn.victory_title"), true); attempt_pet_offer_from_adventure(); } diff --git a/src/combat.nvgt b/src/combat.nvgt index 208fd2b..637bd62 100644 --- a/src/combat.nvgt +++ b/src/combat.nvgt @@ -30,34 +30,36 @@ void speak_ammo_blocked(string message) { speak_with_history(message, true); } -int find_ranged_enemy(int player_x, int range, int direction, bool allow_flying, bool &out hit_bandit, bool &out hit_boar, bool &out hit_flying_creature) { +int find_ranged_enemy(int player_x, int range, int direction, bool allow_flying, bool& out hit_bandit, + bool& out hit_boar, bool& out hit_flying_creature) { hit_bandit = false; hit_boar = false; hit_flying_creature = false; for (int dist = 1; dist <= range; dist++) { int check_x = player_x + (dist * direction); - if (check_x < 0 || check_x >= MAP_SIZE) break; + if (check_x < 0 || check_x >= MAP_SIZE) + break; - Bandit@ bandit = get_bandit_at(check_x); + Bandit @bandit = get_bandit_at(check_x); if (bandit != null) { hit_bandit = true; return check_x; } - GroundGame@ boar = get_boar_at(check_x); + GroundGame @boar = get_boar_at(check_x); if (boar != null) { hit_boar = true; return check_x; } - Undead@ undead = get_zombie_at(check_x); + Undead @undead = get_zombie_at(check_x); if (undead != null) { return check_x; } if (allow_flying) { - FlyingCreature@ creature = get_flying_creature_at(check_x); + FlyingCreature @creature = get_flying_creature_at(check_x); if (creature != null && creature.state == "flying") { hit_flying_creature = true; return check_x; @@ -152,7 +154,7 @@ void perform_sling_attack(int current_x) { } void hit_tree_with_spear(int target_x) { - Tree@ target = get_tree_at(target_x); + Tree @target = get_tree_at(target_x); if (@target != null && !target.is_chopped) { p.play_stationary("sounds/weapons/spear_hit.ogg", false); } @@ -160,7 +162,7 @@ void hit_tree_with_spear(int target_x) { void update_sling_charge() { int elapsed = sling_charge_timer.elapsed; - int cycle_time = 1500; // 1.5 seconds + int cycle_time = 1500; // 1.5 seconds int stage_duration = 500; // 0.5 seconds per stage // Loop the charge cycle @@ -184,13 +186,17 @@ void update_sling_charge() { int get_bow_draw_damage(int elapsed_ms) { int clamped = elapsed_ms; - if (clamped < 0) clamped = 0; - if (clamped > BOW_DRAW_TIME_MS) clamped = BOW_DRAW_TIME_MS; + if (clamped < 0) + clamped = 0; + if (clamped > BOW_DRAW_TIME_MS) + clamped = BOW_DRAW_TIME_MS; float ratio = float(clamped) / float(BOW_DRAW_TIME_MS); int damage = int(ratio * BOW_DAMAGE_MAX); - if (damage < BOW_DAMAGE_MIN) damage = BOW_DAMAGE_MIN; - if (damage > BOW_DAMAGE_MAX) damage = BOW_DAMAGE_MAX; + if (damage < BOW_DAMAGE_MIN) + damage = BOW_DAMAGE_MIN; + if (damage > BOW_DAMAGE_MAX) + damage = BOW_DAMAGE_MAX; return damage; } @@ -213,24 +219,23 @@ void start_bow_shot_audio(int start_x, int end_x, int hit_x, int hit_type, int d bow_shot_duration_ms = duration_ms; bow_shot_drop_pending = false; bow_shot_drop_pos = -1; - if (bow_shot_duration_ms < 1) bow_shot_duration_ms = 1; + if (bow_shot_duration_ms < 1) + bow_shot_duration_ms = 1; - bow_shot_sound_handle = play_1d_with_volume_step( - "sounds/weapons/arrow_flies.ogg", - x, - bow_shot_start_x, - false, - PLAYER_WEAPON_SOUND_VOLUME_STEP - ); + bow_shot_sound_handle = play_1d_with_volume_step("sounds/weapons/arrow_flies.ogg", x, bow_shot_start_x, false, + PLAYER_WEAPON_SOUND_VOLUME_STEP); } void update_bow_shot() { - if (!bow_shot_active) return; - if (bow_shot_duration_ms < 1) bow_shot_duration_ms = 1; + if (!bow_shot_active) + return; + if (bow_shot_duration_ms < 1) + bow_shot_duration_ms = 1; int elapsed = bow_shot_timer.elapsed; float progress = float(elapsed) / float(bow_shot_duration_ms); - if (progress > 1.0f) progress = 1.0f; + if (progress > 1.0f) + progress = 1.0f; int travel = int(float(bow_shot_end_x - bow_shot_start_x) * progress); int current_pos = bow_shot_start_x + travel; @@ -245,13 +250,7 @@ void update_bow_shot() { int drop_pos = bow_shot_drop_pos; stop_bow_shot_audio(); if (hit_x >= 0) { - play_1d_with_volume_step( - "sounds/weapons/arrow_hit.ogg", - x, - hit_x, - false, - PLAYER_WEAPON_SOUND_VOLUME_STEP - ); + play_1d_with_volume_step("sounds/weapons/arrow_hit.ogg", x, hit_x, false, PLAYER_WEAPON_SOUND_VOLUME_STEP); if (hit_type == BOW_HIT_BANDIT) { play_creature_hit_sound("sounds/enemies/zombie_hit.ogg", x, hit_x, BANDIT_SOUND_VOLUME_STEP); } else if (hit_type == BOW_HIT_BOAR) { @@ -283,14 +282,16 @@ void release_bow_attack(int player_x) { bool hit_bandit = false; bool hit_flying_creature = false; bool hit_boar = false; - int target_x = find_ranged_enemy(player_x, BOW_RANGE, search_direction, true, hit_bandit, hit_boar, hit_flying_creature); + int target_x = + find_ranged_enemy(player_x, BOW_RANGE, search_direction, true, hit_bandit, hit_boar, hit_flying_creature); bool hit_tree = false; if (target_x == -1) { for (int dist = 1; dist <= BOW_RANGE; dist++) { int check_x = player_x + (dist * search_direction); - if (check_x < 0 || check_x >= MAP_SIZE) break; - Tree@ tree = get_tree_at(check_x); + if (check_x < 0 || check_x >= MAP_SIZE) + break; + Tree @tree = get_tree_at(check_x); if (tree != null && !tree.is_chopped) { target_x = check_x; hit_tree = true; @@ -318,16 +319,16 @@ void release_bow_attack(int player_x) { hit_type = BOW_HIT_NONE; } - int end_x = (target_x != -1) - ? target_x - : (player_x + (search_direction * (BOW_RANGE + BOW_MISS_EXTRA_TILES))); + int end_x = (target_x != -1) ? target_x : (player_x + (search_direction * (BOW_RANGE + BOW_MISS_EXTRA_TILES))); int duration_ms = ARROW_FLIES_DURATION_MS; if (target_x != -1) { int distance = abs(target_x - player_x); - if (distance < 1) distance = 1; + if (distance < 1) + distance = 1; duration_ms = int(float(ARROW_FLIES_DURATION_MS) * (float(distance) / float(BOW_RANGE))); - if (duration_ms < 1) duration_ms = 1; + if (duration_ms < 1) + duration_ms = 1; } int hit_x = (target_x != -1) ? target_x : -1; @@ -349,7 +350,6 @@ void release_sling_attack(int player_x) { // Only hit if released during in-range window (stage 1) if (stage != 1) { - speak_with_history("Stone missed.", true); return; } @@ -359,17 +359,20 @@ void release_sling_attack(int player_x) { bool hit_bandit = false; bool hit_flying_creature = false; bool hit_boar = false; - target_x = find_ranged_enemy(player_x, SLING_RANGE, search_direction, true, hit_bandit, hit_boar, hit_flying_creature); + target_x = + find_ranged_enemy(player_x, SLING_RANGE, search_direction, true, hit_bandit, hit_boar, hit_flying_creature); // If no enemy found, check for trees (but don't damage them) if (target_x == -1) { for (int dist = 1; dist <= SLING_RANGE; dist++) { int check_x = player_x + (dist * search_direction); - if (check_x < 0 || check_x >= MAP_SIZE) break; - Tree@ tree = get_tree_at(check_x); + if (check_x < 0 || check_x >= MAP_SIZE) + break; + Tree @tree = get_tree_at(check_x); if (tree != null && !tree.is_chopped) { // Stone hits tree but doesn't damage it - play_1d_with_volume_step("sounds/weapons/sling_hit.ogg", player_x, check_x, false, PLAYER_WEAPON_SOUND_VOLUME_STEP); + play_1d_with_volume_step("sounds/weapons/sling_hit.ogg", player_x, check_x, false, + PLAYER_WEAPON_SOUND_VOLUME_STEP); return; } } @@ -377,7 +380,6 @@ void release_sling_attack(int player_x) { // No target found if (target_x == -1) { - speak_with_history("Stone missed.", true); return; } @@ -387,19 +389,23 @@ void release_sling_attack(int player_x) { // Damage the correct enemy type if (hit_bandit) { damage_bandit_at(target_x, damage); - play_1d_with_volume_step("sounds/weapons/sling_hit.ogg", player_x, target_x, false, PLAYER_WEAPON_SOUND_VOLUME_STEP); + play_1d_with_volume_step("sounds/weapons/sling_hit.ogg", player_x, target_x, false, + PLAYER_WEAPON_SOUND_VOLUME_STEP); play_creature_hit_sound("sounds/enemies/zombie_hit.ogg", player_x, target_x, BANDIT_SOUND_VOLUME_STEP); } else if (hit_boar) { damage_boar_at(target_x, damage); - play_1d_with_volume_step("sounds/weapons/sling_hit.ogg", player_x, target_x, false, PLAYER_WEAPON_SOUND_VOLUME_STEP); + play_1d_with_volume_step("sounds/weapons/sling_hit.ogg", player_x, target_x, false, + PLAYER_WEAPON_SOUND_VOLUME_STEP); play_creature_hit_sound("sounds/enemies/zombie_hit.ogg", player_x, target_x, BOAR_SOUND_VOLUME_STEP); } else if (hit_flying_creature) { damage_flying_creature_at(target_x, damage); - play_1d_with_volume_step("sounds/weapons/sling_hit.ogg", player_x, target_x, false, PLAYER_WEAPON_SOUND_VOLUME_STEP); + play_1d_with_volume_step("sounds/weapons/sling_hit.ogg", player_x, target_x, false, + PLAYER_WEAPON_SOUND_VOLUME_STEP); // Falling sound handled by damage_flying_creature_at } else { damage_zombie_at(target_x, damage); - play_1d_with_volume_step("sounds/weapons/sling_hit.ogg", player_x, target_x, false, PLAYER_WEAPON_SOUND_VOLUME_STEP); + play_1d_with_volume_step("sounds/weapons/sling_hit.ogg", player_x, target_x, false, + PLAYER_WEAPON_SOUND_VOLUME_STEP); play_creature_hit_sound("sounds/enemies/zombie_hit.ogg", player_x, target_x, ZOMBIE_SOUND_VOLUME_STEP); } } diff --git a/src/constants.nvgt b/src/constants.nvgt index bc5c6f8..c83138a 100644 --- a/src/constants.nvgt +++ b/src/constants.nvgt @@ -1,14 +1,14 @@ // Map configuration int MAP_SIZE = 35; -const int BASE_END = 4; // 0-4 -const int GRASS_END = 19; // 5-19 +const int BASE_END = 4; // 0-4 +const int GRASS_END = 19; // 5-19 const int GRAVEL_START = 20; // 20-34 -const int GRAVEL_END = 34; // 20-34 +const int GRAVEL_END = 34; // 20-34 // Expansion configuration const int EXPANSION_SIZE = 30; const int EXPANSION_CHANCE = 30; // 30% chance per hour before noon -int expanded_area_start = -1; // -1 means not expanded yet +int expanded_area_start = -1; // -1 means not expanded yet int expanded_area_end = -1; // Movement configuration @@ -32,7 +32,7 @@ const int BLESSING_RESIDENT_DURATION = 300000; const int BLESSING_SEARCH_DURATION = 300000; const int BLESSING_TRIGGER_CHANCE = 10; const int BLESSING_WALK_SPEED = 320; -const int BLESSING_SEARCH_GATHER_BONUS = 30; +const int BLESSING_SEARCH_GATHER_BONUS = 35; const int GATHER_TIME_REDUCTION_CAP = 75; const int FISH_WEIGHT_MIN = 1; const int FISH_WEIGHT_MAX = 30; @@ -193,28 +193,28 @@ const float MASTER_VOLUME_MAX_DB = 0.0; const float MASTER_VOLUME_MIN_DB = -60.0; const float MASTER_VOLUME_STEP_DB = 3.0; const int SNARE_SOUND_RANGE = 2; -const float SNARE_SOUND_VOLUME_STEP = 4.0; // More audible for locating snares -const float SNARE_SOUND_PAN_STEP = 4.0; // Stronger pan for direction +const float SNARE_SOUND_VOLUME_STEP = 4.0; // More audible for locating snares +const float SNARE_SOUND_PAN_STEP = 4.0; // Stronger pan for direction const int SNARE_COLLECT_RANGE = 1; const int FIRE_SOUND_RANGE = 3; -const float FIRE_SOUND_VOLUME_STEP = 5.0; // 15 dB over 3 tiles (FIRE_SOUND_RANGE) +const float FIRE_SOUND_VOLUME_STEP = 5.0; // 15 dB over 3 tiles (FIRE_SOUND_RANGE) const int FIREPIT_SOUND_RANGE = 5; -const float FIREPIT_SOUND_VOLUME_STEP = 6.0; // 30 dB over 5 tiles +const float FIREPIT_SOUND_VOLUME_STEP = 6.0; // 30 dB over 5 tiles const int STREAM_SOUND_RANGE = 7; -const float STREAM_SOUND_VOLUME_STEP = 4.3; // 30 dB over 7 tiles +const float STREAM_SOUND_VOLUME_STEP = 4.3; // 30 dB over 7 tiles -const float TREE_SOUND_VOLUME_STEP = 4.0; // Similar to snares for good audibility +const float TREE_SOUND_VOLUME_STEP = 4.0; // Similar to snares for good audibility const int TREE_SOUND_RANGE = 4; const int TREE_MIN_DISTANCE = 10; const int TREE_MAX_PER_AREA = 2; const int TREE_AVOID_STEEP_CLIMB_RANGE = 3; -const float RESIDENT_DEFENSE_VOLUME_STEP = 3.0; // Default volume for resident counter-attacks +const float RESIDENT_DEFENSE_VOLUME_STEP = 3.0; // Default volume for resident counter-attacks const float PLAYER_WEAPON_SOUND_VOLUME_STEP = 3.0; -const int FLYING_CREATURE_FADE_OUT_DURATION = 1500; // ms +const int FLYING_CREATURE_FADE_OUT_DURATION = 1500; // ms const float FLYING_CREATURE_FADE_OUT_MIN_VOLUME = -40.0; // dB // Mountain configuration @@ -225,7 +225,7 @@ const int MOUNTAIN_STEEP_THRESHOLD = 7; const int MOUNTAIN_MAX_SLOPE = 20; const int ROPE_CLIMB_SPEED = 1000; const int MOUNTAIN_STREAM_SOUND_RANGE = 7; -const float MOUNTAIN_STREAM_VOLUME_STEP = 4.3; // 30 dB over 7 tiles +const float MOUNTAIN_STREAM_VOLUME_STEP = 4.3; // 30 dB over 7 tiles const int QUEST_MAX_ACTIVE = 4; const int QUEST_CHANCE_PER_FAVOR = 10; const int QUEST_MIN_CHANCE = 5; @@ -235,20 +235,20 @@ const int QUEST_LOG_SCORE = 10; const int QUEST_SKIN_SCORE = 14; // Resident settings -const int MAX_RESIDENTS = 4; // Max residents per base (+ player = 5 total) +const int MAX_RESIDENTS = 4; // Max residents per base (+ player = 5 total) const int RESIDENT_WEAPON_BREAK_CHANCE = 10; const int RESIDENT_SPEAR_DAMAGE = 2; const int RESIDENT_SLING_DAMAGE_MIN = 3; const int RESIDENT_SLING_DAMAGE_MAX = 5; const int RESIDENT_BOW_DAMAGE_MIN = 4; const int RESIDENT_BOW_DAMAGE_MAX = 6; -const int RESIDENT_COMBAT_BASE_COOLDOWN = 3200; // Base attack delay (2x player axe) -const int RESIDENT_COMBAT_TARGET_COOLDOWN = 1600; // At max horses, about player axe speed -const int RESIDENT_SNARE_ESCAPE_CHANCE = 5; // 5% chance game escapes when resident retrieves -const int RESIDENT_SNARE_CHECK_CHANCE = 15; // 15% chance per hour to check snares -const int RESIDENT_FISHING_CHANCE = 6; // 6% chance per resident per hour to catch a fish -const int RESIDENT_SMOKE_FISH_CHANCE = 10; // 10% chance per hour to smoke a stored fish -const int RESIDENT_TOOL_BREAK_CHANCE = 2; // 2% chance tools break during resident use (fishing poles, knives, baskets) +const int RESIDENT_COMBAT_BASE_COOLDOWN = 3200; // Base attack delay (2x player axe) +const int RESIDENT_COMBAT_TARGET_COOLDOWN = 1600; // At max horses, about player axe speed +const int RESIDENT_SNARE_ESCAPE_CHANCE = 5; // 5% chance game escapes when resident retrieves +const int RESIDENT_SNARE_CHECK_CHANCE = 15; // 15% chance per hour to check snares +const int RESIDENT_FISHING_CHANCE = 6; // 6% chance per resident per hour to catch a fish +const int RESIDENT_SMOKE_FISH_CHANCE = 10; // 10% chance per hour to smoke a stored fish +const int RESIDENT_TOOL_BREAK_CHANCE = 2; // 2% chance tools break during resident use (fishing poles, knives, baskets) const int RESIDENT_CLOTHING_REPAIR_COST = 5; const float PLAYER_ITEM_BREAK_CHANCE_MIN = 1.0; const float PLAYER_ITEM_BREAK_CHANCE_MAX = 100.0; @@ -266,8 +266,8 @@ const int PET_LOYALTY_MAX = 10; const int PET_ATTACK_COOLDOWN = 1600; // Same as axe const int PET_RETRIEVE_COOLDOWN = 1000; const int PET_RANDOM_FIND_CHANCE = 20; // Percent per hour when loyalty is high -const int PET_ADVENTURE_CHANCE = 5; // Percent chance after adventure victory -const int PET_TREE_HAWK_CHANCE = 10; // Percent chance after reaching top of a tree +const int PET_ADVENTURE_CHANCE = 5; // Percent chance after adventure victory +const int PET_TREE_HAWK_CHANCE = 10; // Percent chance after reaching top of a tree const int PET_LOYALTY_BONUS_THRESHOLD = 5; const int PET_TRAVEL_MIN_MS = 100; const int PET_RANGE = BOW_RANGE + 2; @@ -277,18 +277,19 @@ const int PET_KNOCKOUT_COOLDOWN_HOURS = 4; const float PET_CHARGE_SPEED_MULTIPLIER = 1.5; const int PET_FOLLOW_COMFORT_DISTANCE = 2; const int PET_FOLLOW_MAX_DISTANCE = 5; +const int PET_LEAVE_DESPAWN_DISTANCE = 20; // Goose settings const int GOOSE_HEALTH = 1; -const int GOOSE_MOVE_INTERVAL_MIN = 800; // Faster movement +const int GOOSE_MOVE_INTERVAL_MIN = 800; // Faster movement const int GOOSE_MOVE_INTERVAL_MAX = 2000; const int GOOSE_FLYING_HEIGHT_MIN = 10; const int GOOSE_FLYING_HEIGHT_MAX = 30; const float GOOSE_SOUND_VOLUME_STEP = 3.0; const int GOOSE_FLIGHT_SOUND_DELAY_MIN = 2000; // Honk more often const int GOOSE_FLIGHT_SOUND_DELAY_MAX = 5000; -const int GOOSE_FALL_SPEED = 100; // ms per foot -const int GOOSE_FLY_AWAY_CHANCE = 0; // Chance out of 1000 per tick to fly away +const int GOOSE_FALL_SPEED = 100; // ms per foot +const int GOOSE_FLY_AWAY_CHANCE = 0; // Chance out of 1000 per tick to fly away const int GOOSE_MAX_DIST_FROM_WATER = 4; // How far they can wander from water const int GOOSE_MAX_COUNT = 3; const int GOOSE_HOURLY_SPAWN_CHANCE = 40; // Percent chance per hour to spawn a goose @@ -303,28 +304,28 @@ const int TURKEY_FLYING_HEIGHT_MAX = 30; const float TURKEY_SOUND_VOLUME_STEP = 3.0; const int TURKEY_FLIGHT_SOUND_DELAY_MIN = 2000; const int TURKEY_FLIGHT_SOUND_DELAY_MAX = 5000; -const int TURKEY_FALL_SPEED = 100; // ms per foot -const int TURKEY_FLY_AWAY_CHANCE = 0; // Chance out of 1000 per tick to fly away +const int TURKEY_FALL_SPEED = 100; // ms per foot +const int TURKEY_FLY_AWAY_CHANCE = 0; // Chance out of 1000 per tick to fly away const int TURKEY_MAX_DIST_FROM_FOREST = 0; // How far they can wander from forest const int TURKEY_MAX_COUNT = 3; const int TURKEY_HOURLY_SPAWN_CHANCE = 40; // Percent chance per hour to spawn a turkey const int TURKEY_SIGHT_RANGE = 0; // Weather settings -const int WEATHER_FADE_DURATION = 8000; // 8 seconds for smooth audio transitions +const int WEATHER_FADE_DURATION = 8000; // 8 seconds for smooth audio transitions const float WEATHER_MIN_VOLUME = -30.0; const float WEATHER_MAX_VOLUME = 0.0; const float RAIN_VOLUME_LIGHT = -18.0; const float RAIN_VOLUME_MODERATE = -10.0; const float RAIN_VOLUME_HEAVY = -3.0; -const int WIND_GUST_MIN_DELAY = 30000; // Min 30 seconds between gusts -const int WIND_GUST_MAX_DELAY = 60000; // Max 60 seconds between gusts -const int THUNDER_MIN_INTERVAL = 8000; // Min 8 seconds between thunder -const int THUNDER_MAX_INTERVAL = 35000; // Max 35 seconds between thunder -const int THUNDER_MOVEMENT_SPEED = 250; // ms per tile movement (faster roll across sky) -const float THUNDER_SOUND_VOLUME_STEP = 0.5; // Gentler volume falloff -const int THUNDER_SPAWN_DISTANCE_MIN = 0; // Min distance from player -const int THUNDER_SPAWN_DISTANCE_MAX = 20; // Max distance from player +const int WIND_GUST_MIN_DELAY = 30000; // Min 30 seconds between gusts +const int WIND_GUST_MAX_DELAY = 60000; // Max 60 seconds between gusts +const int THUNDER_MIN_INTERVAL = 8000; // Min 8 seconds between thunder +const int THUNDER_MAX_INTERVAL = 35000; // Max 35 seconds between thunder +const int THUNDER_MOVEMENT_SPEED = 250; // ms per tile movement (faster roll across sky) +const float THUNDER_SOUND_VOLUME_STEP = 0.5; // Gentler volume falloff +const int THUNDER_SPAWN_DISTANCE_MIN = 0; // Min distance from player +const int THUNDER_SPAWN_DISTANCE_MAX = 20; // Max distance from player const int CHANCE_CLEAR_TO_WINDY = 15; const int CHANCE_CLEAR_TO_RAINY = 6; const int CHANCE_CLEAR_TO_STORMY = 5; @@ -357,7 +358,7 @@ const int FALL_DAMAGE_MAX = 4; // Base Automation const int RESIDENT_COLLECTION_CHANCE = 10; // 10% chance per basket per hour -const int RESIDENT_FORAGING_CHANCE = 50; // 50% chance per resident per attempt (daily) +const int RESIDENT_FORAGING_CHANCE = 50; // 50% chance per resident per attempt (daily) // Utility functions int abs(int value) { diff --git a/src/crafting/craft_barricade.nvgt b/src/crafting/craft_barricade.nvgt index 8fcc956..9e9c357 100644 --- a/src/crafting/craft_barricade.nvgt +++ b/src/crafting/craft_barricade.nvgt @@ -1,7 +1,29 @@ // 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 speak_barricade_reinforced_status(const string& in key, int gainedHealth) { + dictionary args; + args.set("gained", gainedHealth); + args.set("health", barricade_health); + args.set("max", BARRICADE_MAX_HEALTH); + speak_with_history(trf(key, args), true); +} + +void speak_barricade_reinforced_max_status(const string& in key, int reinforceCount) { + dictionary args; + args.set("count", reinforceCount); + args.set("health", barricade_health); + speak_with_history(trf(key, args), true); +} + void run_barricade_menu() { if (barricade_health >= BARRICADE_MAX_HEALTH) { - speak_with_history("Barricade is already at full health.", true); + speak_with_history(tr("system.crafting.barricade.full_health"), true); return; } @@ -10,69 +32,84 @@ 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); } if (options.length() == 0) { - speak_with_history("No materials to reinforce the barricade.", true); + speak_with_history(tr("system.crafting.barricade.no_materials"), true); return; } - speak_with_history("Barricade. " + options[selection], true); + speak_menu_prompt("system.crafting.barricade.prompt", options[selection]); - while(true) { + while (true) { wait(5); if (menu_background_tick()) { return; } if (key_pressed(KEY_ESCAPE)) { - speak_with_history("Closed.", true); + speak_with_history(tr("system.menu.closed"), true); break; } 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); } if (key_pressed(KEY_RETURN)) { play_menu_select_sound(); int action = action_types[selection]; - if (action == 0) reinforce_barricade_with_sticks(); - else if (action == 1) reinforce_barricade_with_vines(); - else if (action == 2) reinforce_barricade_with_log(); - else if (action == 3) reinforce_barricade_with_stones(); + if (action == 0) + reinforce_barricade_with_sticks(); + else if (action == 1) + reinforce_barricade_with_vines(); + else if (action == 2) + reinforce_barricade_with_log(); + else if (action == 3) + reinforce_barricade_with_stones(); break; } if (key_pressed(KEY_TAB)) { play_menu_select_sound(); int action = action_types[selection]; - if (action == 0) reinforce_barricade_max_with_sticks(); - else if (action == 1) reinforce_barricade_max_with_vines(); - else if (action == 2) reinforce_barricade_max_with_log(); - else if (action == 3) reinforce_barricade_max_with_stones(); + if (action == 0) + reinforce_barricade_max_with_sticks(); + else if (action == 1) + reinforce_barricade_max_with_vines(); + else if (action == 2) + reinforce_barricade_max_with_log(); + else if (action == 3) + reinforce_barricade_max_with_stones(); break; } } @@ -80,76 +117,76 @@ void run_barricade_menu() { void reinforce_barricade_with_sticks() { if (barricade_health >= BARRICADE_MAX_HEALTH) { - speak_with_history("Barricade is already at full health.", true); + speak_with_history(tr("system.crafting.barricade.full_health"), true); return; } if (get_personal_count(ITEM_STICKS) < BARRICADE_STICK_COST) { - speak_with_history("Not enough sticks.", true); + speak_with_history(tr("system.crafting.barricade.not_enough.sticks"), true); return; } simulate_crafting(BARRICADE_STICK_COST); add_personal_count(ITEM_STICKS, -BARRICADE_STICK_COST); int gained = add_barricade_health(BARRICADE_STICK_HEALTH); - speak_with_history("Reinforced barricade with sticks. +" + gained + " health. Now " + barricade_health + " of " + BARRICADE_MAX_HEALTH + ".", true); + speak_barricade_reinforced_status("system.crafting.barricade.reinforced.sticks", gained); } void reinforce_barricade_with_vines() { if (barricade_health >= BARRICADE_MAX_HEALTH) { - speak_with_history("Barricade is already at full health.", true); + speak_with_history(tr("system.crafting.barricade.full_health"), true); return; } if (get_personal_count(ITEM_VINES) < BARRICADE_VINE_COST) { - speak_with_history("Not enough vines.", true); + speak_with_history(tr("system.crafting.barricade.not_enough.vines"), true); return; } simulate_crafting(BARRICADE_VINE_COST); add_personal_count(ITEM_VINES, -BARRICADE_VINE_COST); int gained = add_barricade_health(BARRICADE_VINE_HEALTH); - speak_with_history("Reinforced barricade with vines. +" + gained + " health. Now " + barricade_health + " of " + BARRICADE_MAX_HEALTH + ".", true); + speak_barricade_reinforced_status("system.crafting.barricade.reinforced.vines", gained); } void reinforce_barricade_with_log() { if (barricade_health >= BARRICADE_MAX_HEALTH) { - speak_with_history("Barricade is already at full health.", true); + speak_with_history(tr("system.crafting.barricade.full_health"), true); return; } if (get_personal_count(ITEM_LOGS) < BARRICADE_LOG_COST) { - speak_with_history("Not enough logs.", true); + speak_with_history(tr("system.crafting.barricade.not_enough.logs"), true); return; } simulate_crafting(BARRICADE_LOG_COST); add_personal_count(ITEM_LOGS, -BARRICADE_LOG_COST); int gained = add_barricade_health(BARRICADE_LOG_HEALTH); - speak_with_history("Reinforced barricade with log. +" + gained + " health. Now " + barricade_health + " of " + BARRICADE_MAX_HEALTH + ".", true); + speak_barricade_reinforced_status("system.crafting.barricade.reinforced.log", gained); } void reinforce_barricade_with_stones() { if (barricade_health >= BARRICADE_MAX_HEALTH) { - speak_with_history("Barricade is already at full health.", true); + speak_with_history(tr("system.crafting.barricade.full_health"), true); return; } if (get_personal_count(ITEM_STONES) < BARRICADE_STONE_COST) { - speak_with_history("Not enough stones.", true); + speak_with_history(tr("system.crafting.barricade.not_enough.stones"), true); return; } simulate_crafting(BARRICADE_STONE_COST); add_personal_count(ITEM_STONES, -BARRICADE_STONE_COST); int gained = add_barricade_health(BARRICADE_STONE_HEALTH); - speak_with_history("Reinforced barricade with stones. +" + gained + " health. Now " + barricade_health + " of " + BARRICADE_MAX_HEALTH + ".", true); + speak_barricade_reinforced_status("system.crafting.barricade.reinforced.stones", gained); } void reinforce_barricade_max_with_sticks() { if (barricade_health >= BARRICADE_MAX_HEALTH) { - speak_with_history("Barricade is already at full health.", true); + speak_with_history(tr("system.crafting.barricade.full_health"), true); return; } 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; } @@ -164,19 +201,20 @@ void reinforce_barricade_max_with_sticks() { simulate_crafting(craft_time); add_personal_count(ITEM_STICKS, -total_cost); barricade_health += (to_do * BARRICADE_STICK_HEALTH); - if (barricade_health > BARRICADE_MAX_HEALTH) barricade_health = BARRICADE_MAX_HEALTH; + if (barricade_health > BARRICADE_MAX_HEALTH) + barricade_health = BARRICADE_MAX_HEALTH; - speak_with_history("Reinforced barricade " + to_do + " times with sticks. Health now " + barricade_health + ".", true); + speak_barricade_reinforced_max_status("system.crafting.barricade.reinforced_max.sticks", to_do); } void reinforce_barricade_max_with_vines() { if (barricade_health >= BARRICADE_MAX_HEALTH) { - speak_with_history("Barricade is already at full health.", true); + speak_with_history(tr("system.crafting.barricade.full_health"), true); return; } 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; } @@ -191,19 +229,20 @@ void reinforce_barricade_max_with_vines() { simulate_crafting(craft_time); add_personal_count(ITEM_VINES, -total_cost); barricade_health += (to_do * BARRICADE_VINE_HEALTH); - if (barricade_health > BARRICADE_MAX_HEALTH) barricade_health = BARRICADE_MAX_HEALTH; + if (barricade_health > BARRICADE_MAX_HEALTH) + barricade_health = BARRICADE_MAX_HEALTH; - speak_with_history("Reinforced barricade " + to_do + " times with vines. Health now " + barricade_health + ".", true); + speak_barricade_reinforced_max_status("system.crafting.barricade.reinforced_max.vines", to_do); } void reinforce_barricade_max_with_log() { if (barricade_health >= BARRICADE_MAX_HEALTH) { - speak_with_history("Barricade is already at full health.", true); + speak_with_history(tr("system.crafting.barricade.full_health"), true); return; } 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; } @@ -218,19 +257,20 @@ void reinforce_barricade_max_with_log() { simulate_crafting(craft_time); add_personal_count(ITEM_LOGS, -total_cost); barricade_health += (to_do * BARRICADE_LOG_HEALTH); - if (barricade_health > BARRICADE_MAX_HEALTH) barricade_health = BARRICADE_MAX_HEALTH; + if (barricade_health > BARRICADE_MAX_HEALTH) + barricade_health = BARRICADE_MAX_HEALTH; - speak_with_history("Reinforced barricade " + to_do + " times with log. Health now " + barricade_health + ".", true); + speak_barricade_reinforced_max_status("system.crafting.barricade.reinforced_max.log", to_do); } void reinforce_barricade_max_with_stones() { if (barricade_health >= BARRICADE_MAX_HEALTH) { - speak_with_history("Barricade is already at full health.", true); + speak_with_history(tr("system.crafting.barricade.full_health"), true); return; } 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; } @@ -245,7 +285,8 @@ void reinforce_barricade_max_with_stones() { simulate_crafting(craft_time); add_personal_count(ITEM_STONES, -total_cost); barricade_health += (to_do * BARRICADE_STONE_HEALTH); - if (barricade_health > BARRICADE_MAX_HEALTH) barricade_health = BARRICADE_MAX_HEALTH; + if (barricade_health > BARRICADE_MAX_HEALTH) + barricade_health = BARRICADE_MAX_HEALTH; - speak_with_history("Reinforced barricade " + to_do + " times with stones. Health now " + barricade_health + ".", true); + speak_barricade_reinforced_max_status("system.crafting.barricade.reinforced_max.stones", to_do); } diff --git a/src/crafting/craft_buildings.nvgt b/src/crafting/craft_buildings.nvgt index 2e09ea5..a57c606 100644 --- a/src/crafting/craft_buildings.nvgt +++ b/src/crafting/craft_buildings.nvgt @@ -15,13 +15,20 @@ bool has_building_options() { } } - if (x > BASE_END || !base_has_firepit) return true; - if (x > BASE_END || !base_has_fire) return true; - if (get_herb_garden_at_base() == null) return true; - if (storage_level < STORAGE_LEVEL_UPGRADE_2) return true; - if (world_pastures.length() == 0 && storage_level >= STORAGE_LEVEL_UPGRADE_1) return true; - if (world_stables.length() == 0 && storage_level >= STORAGE_LEVEL_UPGRADE_1) return true; - if (world_altars.length() == 0) return true; + if (x > BASE_END || !base_has_firepit) + return true; + if (x > BASE_END || !base_has_fire) + return true; + if (get_herb_garden_at_base() == null) + return true; + if (storage_level < STORAGE_LEVEL_UPGRADE_2) + return true; + if (world_pastures.length() == 0 && storage_level >= STORAGE_LEVEL_UPGRADE_1) + return true; + if (world_stables.length() == 0 && storage_level >= STORAGE_LEVEL_UPGRADE_1) + return true; + if (world_altars.length() == 0) + return true; return false; } @@ -48,87 +55,96 @@ 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); } if (options.length() == 0) { - speak_with_history("No buildings available.", true); + speak_with_history(tr("system.crafting.buildings.none_available"), true); return; } - speak_with_history("Buildings. " + options[selection], true); + speak_menu_prompt("system.crafting.buildings.prompt", options[selection]); - while(true) { + while (true) { wait(5); if (menu_background_tick()) { return; } if (key_pressed(KEY_ESCAPE)) { - speak_with_history("Closed.", true); + speak_with_history(tr("system.menu.closed"), true); break; } 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); } if (key_pressed(KEY_RETURN)) { play_menu_select_sound(); int building = building_types[selection]; - if (building == 0) craft_firepit(); - else if (building == 1) craft_campfire(); - else if (building == 2) craft_herb_garden(); - else if (building == 3) craft_storage(); - else if (building == 4) craft_pasture(); - else if (building == 5) craft_stable(); - else if (building == 6) craft_altar(); + if (building == 0) + craft_firepit(); + else if (building == 1) + craft_campfire(); + else if (building == 2) + craft_herb_garden(); + else if (building == 3) + craft_storage(); + else if (building == 4) + craft_pasture(); + else if (building == 5) + craft_stable(); + else if (building == 6) + craft_altar(); break; } } @@ -139,7 +155,7 @@ void craft_firepit() { if (x <= BASE_END) { for (uint i = 0; i < world_firepits.length(); i++) { if (world_firepits[i].position <= BASE_END) { - speak_with_history("There is already a firepit in the base.", true); + speak_with_history(tr("system.crafting.buildings.firepit.already_in_base"), true); return; } } @@ -147,28 +163,29 @@ void craft_firepit() { // Check if there's already a firepit here if (get_firepit_at(x) != null) { - speak_with_history("There is already a firepit here.", true); + speak_with_history(tr("system.crafting.buildings.firepit.already_here"), true); return; } string missing = ""; - if (get_personal_count(ITEM_STONES) < 9) missing += "9 stones "; + if (get_personal_count(ITEM_STONES) < 9) + missing += "9 stones "; if (missing == "") { simulate_crafting(9); add_personal_count(ITEM_STONES, -9); add_world_firepit(x); - speak_with_history("Firepit built here.", true); + speak_with_history(tr("system.crafting.buildings.firepit.built"), true); } else { - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); } } void craft_campfire() { // Check if there's a firepit within 2 tiles - WorldFirepit@ firepit = get_firepit_near(x, 2); + WorldFirepit @firepit = get_firepit_near(x, 2); if (firepit == null) { - speak_with_history("You need a firepit within 2 tiles to build a fire.", true); + speak_with_history(tr("system.crafting.buildings.fire.requires_firepit_nearby"), true); return; } @@ -176,15 +193,17 @@ void craft_campfire() { if (firepit.position <= BASE_END) { for (uint i = 0; i < world_fires.length(); i++) { if (world_fires[i].position <= BASE_END) { - speak_with_history("There is already a fire in the base.", true); + speak_with_history(tr("system.crafting.buildings.fire.already_in_base"), true); return; } } } string missing = ""; - if (get_personal_count(ITEM_LOGS) < 1) missing += "1 log "; - if (get_personal_count(ITEM_STICKS) < 2) missing += "2 sticks "; + if (get_personal_count(ITEM_LOGS) < 1) + missing += "1 log "; + if (get_personal_count(ITEM_STICKS) < 2) + missing += "2 sticks "; if (missing == "") { simulate_crafting(3); @@ -192,29 +211,32 @@ void craft_campfire() { add_personal_count(ITEM_STICKS, -2); // Build the fire at the firepit location, not player location add_world_fire(firepit.position); - speak_with_history("Fire built at firepit.", true); + speak_with_history(tr("system.crafting.buildings.fire.built"), true); } else { - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); } } void craft_herb_garden() { // Can only build in base area if (x > BASE_END) { - speak_with_history("Herb garden can only be built in the base area.", true); + speak_with_history(tr("system.crafting.buildings.herb_garden.base_only"), true); return; } // Check if there's already an herb garden in the base if (get_herb_garden_at_base() != null) { - speak_with_history("There is already an herb garden in the base.", true); + speak_with_history(tr("system.crafting.buildings.herb_garden.already_in_base"), true); return; } string missing = ""; - if (get_personal_count(ITEM_STONES) < 9) missing += "9 stones "; - if (get_personal_count(ITEM_VINES) < 3) missing += "3 vines "; - if (get_personal_count(ITEM_LOGS) < 2) missing += "2 logs "; + if (get_personal_count(ITEM_STONES) < 9) + missing += "9 stones "; + if (get_personal_count(ITEM_VINES) < 3) + missing += "3 vines "; + if (get_personal_count(ITEM_LOGS) < 2) + missing += "2 logs "; if (missing == "") { simulate_crafting(14); @@ -222,19 +244,19 @@ void craft_herb_garden() { add_personal_count(ITEM_VINES, -3); add_personal_count(ITEM_LOGS, -2); add_world_herb_garden(x); - speak_with_history("Herb garden built. The base now heals faster.", true); + speak_with_history(tr("system.crafting.buildings.herb_garden.built"), true); } else { - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); } } void craft_storage() { if (x > BASE_END) { - speak_with_history("Storage must be built in the base.", true); + speak_with_history(tr("system.crafting.buildings.storage.base_only"), true); return; } if (storage_level >= STORAGE_LEVEL_UPGRADE_2) { - speak_with_history("Storage is fully upgraded.", true); + speak_with_history(tr("system.crafting.buildings.storage.fully_upgraded"), true); return; } string missing = ""; @@ -252,9 +274,12 @@ void craft_storage() { newCapacity = BASE_STORAGE_UPGRADE_2_MAX; craftTime = 46; } - if (get_personal_count(ITEM_LOGS) < logCost) missing += logCost + " logs "; - if (get_personal_count(ITEM_STONES) < stoneCost) missing += stoneCost + " stones "; - if (get_personal_count(ITEM_VINES) < vineCost) missing += vineCost + " vines "; + if (get_personal_count(ITEM_LOGS) < logCost) + missing += logCost + " logs "; + if (get_personal_count(ITEM_STONES) < stoneCost) + missing += stoneCost + " stones "; + if (get_personal_count(ITEM_VINES) < vineCost) + missing += vineCost + " vines "; if (missing == "") { simulate_crafting(craftTime); @@ -265,57 +290,64 @@ void craft_storage() { add_world_storage(x); } storage_level = targetLevel; - speak_with_history("Storage upgraded. Capacity is now " + newCapacity + " per item.", true); + dictionary storageArgs; + storageArgs.set("capacity", newCapacity); + speak_with_history(trf("system.crafting.buildings.storage.upgraded", storageArgs), true); } else { - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); } } void craft_pasture() { if (x > BASE_END) { - speak_with_history("Pasture must be built in the base.", true); + speak_with_history(tr("system.crafting.buildings.pasture.base_only"), true); return; } if (storage_level < STORAGE_LEVEL_UPGRADE_1) { - speak_with_history("Storage must be upgraded before a pasture.", true); + speak_with_history(tr("system.crafting.buildings.pasture.requires_storage_upgrade"), true); return; } if (world_pastures.length() > 0) { - speak_with_history("Pasture already built.", true); + speak_with_history(tr("system.crafting.buildings.pasture.already_built"), true); return; } string missing = ""; - if (get_personal_count(ITEM_LOGS) < PASTURE_LOG_COST) missing += PASTURE_LOG_COST + " logs "; - if (get_personal_count(ITEM_ROPES) < PASTURE_ROPE_COST) missing += PASTURE_ROPE_COST + " ropes "; + if (get_personal_count(ITEM_LOGS) < PASTURE_LOG_COST) + missing += PASTURE_LOG_COST + " logs "; + if (get_personal_count(ITEM_ROPES) < PASTURE_ROPE_COST) + missing += PASTURE_ROPE_COST + " ropes "; if (missing == "") { simulate_crafting(28); add_personal_count(ITEM_LOGS, -PASTURE_LOG_COST); add_personal_count(ITEM_ROPES, -PASTURE_ROPE_COST); add_world_pasture(x); - speak_with_history("Pasture built.", true); + speak_with_history(tr("system.crafting.buildings.pasture.built"), true); } else { - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); } } void craft_stable() { if (x > BASE_END) { - speak_with_history("Stable must be built in the base.", true); + speak_with_history(tr("system.crafting.buildings.stable.base_only"), true); return; } if (storage_level < STORAGE_LEVEL_UPGRADE_1) { - speak_with_history("Storage must be upgraded before a stable.", true); + speak_with_history(tr("system.crafting.buildings.stable.requires_storage_upgrade"), true); return; } if (world_stables.length() > 0) { - speak_with_history("Stable already built.", true); + speak_with_history(tr("system.crafting.buildings.stable.already_built"), true); return; } string missing = ""; - if (get_personal_count(ITEM_LOGS) < STABLE_LOG_COST) missing += STABLE_LOG_COST + " logs "; - if (get_personal_count(ITEM_STONES) < STABLE_STONE_COST) missing += STABLE_STONE_COST + " stones "; - if (get_personal_count(ITEM_VINES) < STABLE_VINE_COST) missing += STABLE_VINE_COST + " vines "; + if (get_personal_count(ITEM_LOGS) < STABLE_LOG_COST) + missing += STABLE_LOG_COST + " logs "; + if (get_personal_count(ITEM_STONES) < STABLE_STONE_COST) + missing += STABLE_STONE_COST + " stones "; + if (get_personal_count(ITEM_VINES) < STABLE_VINE_COST) + missing += STABLE_VINE_COST + " vines "; if (missing == "") { simulate_crafting(35); @@ -323,32 +355,34 @@ void craft_stable() { add_personal_count(ITEM_STONES, -STABLE_STONE_COST); add_personal_count(ITEM_VINES, -STABLE_VINE_COST); add_world_stable(x); - speak_with_history("Stable built.", true); + speak_with_history(tr("system.crafting.buildings.stable.built"), true); } else { - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); } } void craft_altar() { if (x > BASE_END) { - speak_with_history("Altar must be built in the base.", true); + speak_with_history(tr("system.crafting.buildings.altar.base_only"), true); return; } if (world_altars.length() > 0) { - speak_with_history("Altar already built.", true); + speak_with_history(tr("system.crafting.buildings.altar.already_built"), true); return; } string missing = ""; - if (get_personal_count(ITEM_STONES) < ALTAR_STONE_COST) missing += ALTAR_STONE_COST + " stones "; - if (get_personal_count(ITEM_STICKS) < ALTAR_STICK_COST) missing += ALTAR_STICK_COST + " sticks "; + if (get_personal_count(ITEM_STONES) < ALTAR_STONE_COST) + missing += ALTAR_STONE_COST + " stones "; + if (get_personal_count(ITEM_STICKS) < ALTAR_STICK_COST) + missing += ALTAR_STICK_COST + " sticks "; if (missing == "") { simulate_crafting(12); add_personal_count(ITEM_STONES, -ALTAR_STONE_COST); add_personal_count(ITEM_STICKS, -ALTAR_STICK_COST); add_world_altar(x); - speak_with_history("Altar built.", true); + speak_with_history(tr("system.crafting.buildings.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 c9fffa8..165033a 100644 --- a/src/crafting/craft_clothing.nvgt +++ b/src/crafting/craft_clothing.nvgt @@ -44,62 +44,76 @@ 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) { + while (true) { wait(5); if (menu_background_tick()) { return; } if (key_pressed(KEY_ESCAPE)) { - speak_with_history("Closed.", true); + speak_with_history(tr("system.menu.closed"), true); break; } 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); } if (key_pressed(KEY_RETURN)) { play_menu_select_sound(); - if (selection == 0) craft_skin_hat(); - else if (selection == 1) craft_skin_gloves(); - else if (selection == 2) craft_skin_pants(); - else if (selection == 3) craft_skin_tunic(); - else if (selection == 4) craft_moccasins(); - else if (selection == 5) craft_skin_pouch(); - else if (selection == 6) craft_backpack(); + if (selection == 0) + craft_skin_hat(); + else if (selection == 1) + craft_skin_gloves(); + else if (selection == 2) + craft_skin_pants(); + else if (selection == 3) + craft_skin_tunic(); + else if (selection == 4) + craft_moccasins(); + else if (selection == 5) + craft_skin_pouch(); + else if (selection == 6) + craft_backpack(); break; } if (key_pressed(KEY_TAB)) { play_menu_select_sound(); - if (selection == 0) craft_skin_hat_max(); - else if (selection == 1) craft_skin_gloves_max(); - else if (selection == 2) craft_skin_pants_max(); - else if (selection == 3) craft_skin_tunic_max(); - else if (selection == 4) craft_moccasins_max(); - else if (selection == 5) craft_skin_pouch_max(); - else if (selection == 6) craft_backpack_max(); + if (selection == 0) + craft_skin_hat_max(); + else if (selection == 1) + craft_skin_gloves_max(); + else if (selection == 2) + craft_skin_pants_max(); + else if (selection == 3) + craft_skin_tunic_max(); + else if (selection == 4) + craft_moccasins_max(); + else if (selection == 5) + craft_skin_pouch_max(); + else if (selection == 6) + craft_backpack_max(); break; } } @@ -107,43 +121,49 @@ void run_clothing_menu() { void craft_skin_hat() { string missing = ""; - if (get_personal_count(ITEM_SKINS) < 1) missing += "1 skin "; - if (get_personal_count(ITEM_VINES) < 1) missing += "1 vine "; + if (get_personal_count(ITEM_SKINS) < 1) + missing += "1 skin "; + if (get_personal_count(ITEM_VINES) < 1) + missing += "1 vine "; 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); add_personal_count(ITEM_SKINS, -1); add_personal_count(ITEM_VINES, -1); add_personal_count(ITEM_SKIN_HATS, 1); - speak_with_history("Crafted a Skin Hat.", true); + speak_with_history(tr("system.crafting.clothing.crafted.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; } int max_by_skins = get_personal_count(ITEM_SKINS); int max_by_vines = get_personal_count(ITEM_VINES); int max_craft = max_by_skins; - if (max_by_vines < max_craft) max_craft = max_by_vines; + if (max_by_vines < max_craft) + max_craft = max_by_vines; int space = get_personal_stack_limit() - get_personal_count(ITEM_SKIN_HATS); - if (max_craft > space) max_craft = space; + if (max_craft > space) + max_craft = space; if (max_craft <= 0) { string missing = ""; - if (get_personal_count(ITEM_SKINS) < 1) missing += "1 skin "; - if (get_personal_count(ITEM_VINES) < 1) missing += "1 vine "; - speak_with_history("Missing: " + missing, true); + if (get_personal_count(ITEM_SKINS) < 1) + missing += "1 skin "; + if (get_personal_count(ITEM_VINES) < 1) + missing += "1 vine "; + speak_crafting_missing(missing); return; } @@ -153,48 +173,56 @@ void craft_skin_hat_max() { add_personal_count(ITEM_SKINS, -max_craft); add_personal_count(ITEM_VINES, -max_craft); add_personal_count(ITEM_SKIN_HATS, max_craft); - speak_with_history("Crafted " + max_craft + " Skin Hats.", true); + dictionary skinHatArgs; + skinHatArgs.set("count", max_craft); + speak_with_history(trf("system.crafting.clothing.crafted_max.skin_hats", skinHatArgs), true); } void craft_skin_gloves() { string missing = ""; - if (get_personal_count(ITEM_SKINS) < 1) missing += "1 skin "; - if (get_personal_count(ITEM_VINES) < 1) missing += "1 vine "; + if (get_personal_count(ITEM_SKINS) < 1) + missing += "1 skin "; + if (get_personal_count(ITEM_VINES) < 1) + missing += "1 vine "; 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); add_personal_count(ITEM_SKINS, -1); add_personal_count(ITEM_VINES, -1); add_personal_count(ITEM_SKIN_GLOVES, 1); - speak_with_history("Crafted Skin Gloves.", true); + speak_with_history(tr("system.crafting.clothing.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; } int max_by_skins = get_personal_count(ITEM_SKINS); int max_by_vines = get_personal_count(ITEM_VINES); int max_craft = max_by_skins; - if (max_by_vines < max_craft) max_craft = max_by_vines; + if (max_by_vines < max_craft) + max_craft = max_by_vines; int space = get_personal_stack_limit() - get_personal_count(ITEM_SKIN_GLOVES); - if (max_craft > space) max_craft = space; + if (max_craft > space) + max_craft = space; if (max_craft <= 0) { string missing = ""; - if (get_personal_count(ITEM_SKINS) < 1) missing += "1 skin "; - if (get_personal_count(ITEM_VINES) < 1) missing += "1 vine "; - speak_with_history("Missing: " + missing, true); + if (get_personal_count(ITEM_SKINS) < 1) + missing += "1 skin "; + if (get_personal_count(ITEM_VINES) < 1) + missing += "1 vine "; + speak_crafting_missing(missing); return; } @@ -204,48 +232,56 @@ void craft_skin_gloves_max() { add_personal_count(ITEM_SKINS, -max_craft); add_personal_count(ITEM_VINES, -max_craft); add_personal_count(ITEM_SKIN_GLOVES, max_craft); - speak_with_history("Crafted " + max_craft + " Skin Gloves.", true); + dictionary skinGlovesArgs; + skinGlovesArgs.set("count", max_craft); + speak_with_history(trf("system.crafting.clothing.crafted_max.skin_gloves", skinGlovesArgs), true); } void craft_skin_pants() { string missing = ""; - if (get_personal_count(ITEM_SKINS) < 6) missing += "6 skins "; - if (get_personal_count(ITEM_VINES) < 3) missing += "3 vines "; + if (get_personal_count(ITEM_SKINS) < 6) + missing += "6 skins "; + if (get_personal_count(ITEM_VINES) < 3) + missing += "3 vines "; 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); add_personal_count(ITEM_SKINS, -6); add_personal_count(ITEM_VINES, -3); add_personal_count(ITEM_SKIN_PANTS, 1); - speak_with_history("Crafted Skin Pants.", true); + speak_with_history(tr("system.crafting.clothing.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; } int max_by_skins = get_personal_count(ITEM_SKINS) / 6; int max_by_vines = get_personal_count(ITEM_VINES) / 3; int max_craft = max_by_skins; - if (max_by_vines < max_craft) max_craft = max_by_vines; + if (max_by_vines < max_craft) + max_craft = max_by_vines; int space = get_personal_stack_limit() - get_personal_count(ITEM_SKIN_PANTS); - if (max_craft > space) max_craft = space; + if (max_craft > space) + max_craft = space; if (max_craft <= 0) { string missing = ""; - if (get_personal_count(ITEM_SKINS) < 6) missing += "6 skins "; - if (get_personal_count(ITEM_VINES) < 3) missing += "3 vines "; - speak_with_history("Missing: " + missing, true); + if (get_personal_count(ITEM_SKINS) < 6) + missing += "6 skins "; + if (get_personal_count(ITEM_VINES) < 3) + missing += "3 vines "; + speak_crafting_missing(missing); return; } @@ -254,48 +290,56 @@ void craft_skin_pants_max() { add_personal_count(ITEM_SKINS, -(max_craft * 6)); add_personal_count(ITEM_VINES, -(max_craft * 3)); add_personal_count(ITEM_SKIN_PANTS, max_craft); - speak_with_history("Crafted " + max_craft + " Skin Pants.", true); + dictionary skinPantsArgs; + skinPantsArgs.set("count", max_craft); + speak_with_history(trf("system.crafting.clothing.crafted_max.skin_pants", skinPantsArgs), true); } void craft_skin_tunic() { string missing = ""; - if (get_personal_count(ITEM_SKINS) < 4) missing += "4 skins "; - if (get_personal_count(ITEM_VINES) < 2) missing += "2 vines "; + if (get_personal_count(ITEM_SKINS) < 4) + missing += "4 skins "; + if (get_personal_count(ITEM_VINES) < 2) + missing += "2 vines "; 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); add_personal_count(ITEM_SKINS, -4); add_personal_count(ITEM_VINES, -2); add_personal_count(ITEM_SKIN_TUNICS, 1); - speak_with_history("Crafted a Skin Tunic.", true); + speak_with_history(tr("system.crafting.clothing.crafted.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; } int max_by_skins = get_personal_count(ITEM_SKINS) / 4; int max_by_vines = get_personal_count(ITEM_VINES) / 2; int max_craft = max_by_skins; - if (max_by_vines < max_craft) max_craft = max_by_vines; + if (max_by_vines < max_craft) + max_craft = max_by_vines; int space = get_personal_stack_limit() - get_personal_count(ITEM_SKIN_TUNICS); - if (max_craft > space) max_craft = space; + if (max_craft > space) + max_craft = space; if (max_craft <= 0) { string missing = ""; - if (get_personal_count(ITEM_SKINS) < 4) missing += "4 skins "; - if (get_personal_count(ITEM_VINES) < 2) missing += "2 vines "; - speak_with_history("Missing: " + missing, true); + if (get_personal_count(ITEM_SKINS) < 4) + missing += "4 skins "; + if (get_personal_count(ITEM_VINES) < 2) + missing += "2 vines "; + speak_crafting_missing(missing); return; } @@ -304,48 +348,56 @@ void craft_skin_tunic_max() { add_personal_count(ITEM_SKINS, -(max_craft * 4)); add_personal_count(ITEM_VINES, -(max_craft * 2)); add_personal_count(ITEM_SKIN_TUNICS, max_craft); - speak_with_history("Crafted " + max_craft + " Skin Tunics.", true); + dictionary skinTunicArgs; + skinTunicArgs.set("count", max_craft); + speak_with_history(trf("system.crafting.clothing.crafted_max.skin_tunics", skinTunicArgs), true); } void craft_moccasins() { string missing = ""; - if (get_personal_count(ITEM_SKINS) < 2) missing += "2 skins "; - if (get_personal_count(ITEM_VINES) < 1) missing += "1 vine "; + if (get_personal_count(ITEM_SKINS) < 2) + missing += "2 skins "; + if (get_personal_count(ITEM_VINES) < 1) + missing += "1 vine "; 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); add_personal_count(ITEM_SKINS, -2); add_personal_count(ITEM_VINES, -1); add_personal_count(ITEM_MOCCASINS, 1); - speak_with_history("Crafted moccasins.", true); + speak_with_history(tr("system.crafting.clothing.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; } int max_by_skins = get_personal_count(ITEM_SKINS) / 2; int max_by_vines = get_personal_count(ITEM_VINES); int max_craft = max_by_skins; - if (max_by_vines < max_craft) max_craft = max_by_vines; + if (max_by_vines < max_craft) + max_craft = max_by_vines; int space = get_personal_stack_limit() - get_personal_count(ITEM_MOCCASINS); - if (max_craft > space) max_craft = space; + if (max_craft > space) + max_craft = space; if (max_craft <= 0) { string missing = ""; - if (get_personal_count(ITEM_SKINS) < 2) missing += "2 skins "; - if (get_personal_count(ITEM_VINES) < 1) missing += "1 vine "; - speak_with_history("Missing: " + missing, true); + if (get_personal_count(ITEM_SKINS) < 2) + missing += "2 skins "; + if (get_personal_count(ITEM_VINES) < 1) + missing += "1 vine "; + speak_crafting_missing(missing); return; } @@ -355,48 +407,56 @@ void craft_moccasins_max() { add_personal_count(ITEM_SKINS, -(max_craft * 2)); add_personal_count(ITEM_VINES, -max_craft); add_personal_count(ITEM_MOCCASINS, max_craft); - speak_with_history("Crafted " + max_craft + " Moccasins.", true); + dictionary moccasinArgs; + moccasinArgs.set("count", max_craft); + speak_with_history(trf("system.crafting.clothing.crafted_max.moccasins", moccasinArgs), true); } void craft_skin_pouch() { string missing = ""; - if (get_personal_count(ITEM_SKINS) < 2) missing += "2 skins "; - if (get_personal_count(ITEM_VINES) < 1) missing += "1 vine "; + if (get_personal_count(ITEM_SKINS) < 2) + missing += "2 skins "; + if (get_personal_count(ITEM_VINES) < 1) + missing += "1 vine "; 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); add_personal_count(ITEM_SKINS, -2); add_personal_count(ITEM_VINES, -1); add_personal_count(ITEM_SKIN_POUCHES, 1); - speak_with_history("Crafted a Skin Pouch.", true); + speak_with_history(tr("system.crafting.clothing.crafted.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; } int max_by_skins = get_personal_count(ITEM_SKINS) / 2; int max_by_vines = get_personal_count(ITEM_VINES); int max_craft = max_by_skins; - if (max_by_vines < max_craft) max_craft = max_by_vines; + if (max_by_vines < max_craft) + max_craft = max_by_vines; int space = get_personal_stack_limit() - get_personal_count(ITEM_SKIN_POUCHES); - if (max_craft > space) max_craft = space; + if (max_craft > space) + max_craft = space; if (max_craft <= 0) { string missing = ""; - if (get_personal_count(ITEM_SKINS) < 2) missing += "2 skins "; - if (get_personal_count(ITEM_VINES) < 1) missing += "1 vine "; - speak_with_history("Missing: " + missing, true); + if (get_personal_count(ITEM_SKINS) < 2) + missing += "2 skins "; + if (get_personal_count(ITEM_VINES) < 1) + missing += "1 vine "; + speak_crafting_missing(missing); return; } @@ -406,18 +466,23 @@ void craft_skin_pouch_max() { add_personal_count(ITEM_SKINS, -(max_craft * 2)); add_personal_count(ITEM_VINES, -max_craft); add_personal_count(ITEM_SKIN_POUCHES, max_craft); - speak_with_history("Crafted " + max_craft + " Skin Pouches.", true); + dictionary skinPouchArgs; + skinPouchArgs.set("count", max_craft); + speak_with_history(trf("system.crafting.clothing.crafted_max.skin_pouches", skinPouchArgs), true); } void craft_backpack() { string missing = ""; - if (get_personal_count(ITEM_SKINS) < 11) missing += "11 skins "; - if (get_personal_count(ITEM_VINES) < 5) missing += "5 vines "; - if (get_total_pouch_count() < 4) missing += "4 skin pouches "; + if (get_personal_count(ITEM_SKINS) < 11) + missing += "11 skins "; + if (get_personal_count(ITEM_VINES) < 5) + missing += "5 vines "; + if (get_total_pouch_count() < 4) + missing += "4 skin pouches "; 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); @@ -425,15 +490,15 @@ void craft_backpack() { add_personal_count(ITEM_VINES, -5); consume_pouches(4); add_personal_count(ITEM_BACKPACKS, 1); - speak_with_history("Crafted a Backpack.", true); + speak_with_history(tr("system.crafting.clothing.crafted.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; } @@ -441,18 +506,24 @@ void craft_backpack_max() { int max_by_vines = get_personal_count(ITEM_VINES) / 5; int max_by_pouches = get_total_pouch_count() / 4; int max_craft = max_by_skins; - if (max_by_vines < max_craft) max_craft = max_by_vines; - if (max_by_pouches < max_craft) max_craft = max_by_pouches; + if (max_by_vines < max_craft) + max_craft = max_by_vines; + if (max_by_pouches < max_craft) + max_craft = max_by_pouches; int space = get_personal_stack_limit() - get_personal_count(ITEM_BACKPACKS); - if (max_craft > space) max_craft = space; + if (max_craft > space) + max_craft = space; if (max_craft <= 0) { string missing = ""; - if (get_personal_count(ITEM_SKINS) < 11) missing += "11 skins "; - if (get_personal_count(ITEM_VINES) < 5) missing += "5 vines "; - if (get_total_pouch_count() < 4) missing += "4 skin pouches "; - speak_with_history("Missing: " + missing, true); + if (get_personal_count(ITEM_SKINS) < 11) + missing += "11 skins "; + if (get_personal_count(ITEM_VINES) < 5) + missing += "5 vines "; + if (get_total_pouch_count() < 4) + missing += "4 skin pouches "; + speak_crafting_missing(missing); return; } @@ -462,5 +533,7 @@ void craft_backpack_max() { add_personal_count(ITEM_VINES, -(max_craft * 5)); consume_pouches(max_craft * 4); add_personal_count(ITEM_BACKPACKS, max_craft); - speak_with_history("Crafted " + max_craft + " Backpacks.", true); + dictionary backpackArgs; + backpackArgs.set("count", max_craft); + speak_with_history(trf("system.crafting.clothing.crafted_max.backpacks", backpackArgs), true); } diff --git a/src/crafting/craft_materials.nvgt b/src/crafting/craft_materials.nvgt index f631e02..a5f4d75 100644 --- a/src/crafting/craft_materials.nvgt +++ b/src/crafting/craft_materials.nvgt @@ -1,56 +1,66 @@ // 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) { + while (true) { wait(5); if (menu_background_tick()) { return; } if (key_pressed(KEY_ESCAPE)) { - speak_with_history("Closed.", true); + speak_with_history(tr("system.menu.closed"), true); break; } 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); } if (key_pressed(KEY_RETURN)) { play_menu_select_sound(); - if (selection == 0) butcher_small_game(); - else if (selection == 1) craft_smoke_fish(); - else if (selection == 2) craft_arrows(); - else if (selection == 3) craft_bowstring(); - else if (selection == 4) craft_incense(); + if (selection == 0) + butcher_small_game(); + else if (selection == 1) + craft_smoke_fish(); + else if (selection == 2) + craft_arrows(); + else if (selection == 3) + craft_bowstring(); + else if (selection == 4) + craft_incense(); break; } if (key_pressed(KEY_TAB)) { play_menu_select_sound(); - if (selection == 0) butcher_small_game_max(); - else if (selection == 1) craft_smoke_fish_max(); - else if (selection == 2) craft_arrows_max(); - else if (selection == 3) craft_bowstring_max(); - else if (selection == 4) craft_incense_max(); + if (selection == 0) + butcher_small_game_max(); + else if (selection == 1) + craft_smoke_fish_max(); + else if (selection == 2) + craft_arrows_max(); + else if (selection == 3) + craft_bowstring_max(); + else if (selection == 4) + craft_incense_max(); break; } } @@ -58,21 +68,24 @@ void run_materials_menu() { void craft_arrows() { if (get_personal_count(ITEM_QUIVERS) <= 0) { - speak_with_history("You need a quiver to craft arrows.", true); + speak_with_history(tr("system.inventory.need_quiver_for_arrows"), true); return; } int currentArrows = get_personal_count(ITEM_ARROWS); int capacity = get_arrow_limit() - currentArrows; if (capacity < ARROWS_PER_CRAFT) { - speak_with_history("Not enough quiver capacity for arrows.", true); + speak_with_history(tr("system.crafting.materials.not_enough_quiver_capacity_for_arrows"), true); return; } string missing = ""; - if (get_personal_count(ITEM_STICKS) < 2) missing += "2 sticks "; - if (get_personal_count(ITEM_FEATHERS) < 4) missing += "4 feathers "; - if (get_personal_count(ITEM_STONES) < 2) missing += "2 stones "; + if (get_personal_count(ITEM_STICKS) < 2) + missing += "2 sticks "; + if (get_personal_count(ITEM_FEATHERS) < 4) + missing += "4 feathers "; + if (get_personal_count(ITEM_STONES) < 2) + missing += "2 stones "; if (missing == "") { simulate_crafting(8); @@ -80,15 +93,17 @@ void craft_arrows() { add_personal_count(ITEM_FEATHERS, -4); add_personal_count(ITEM_STONES, -2); add_personal_count(ITEM_ARROWS, ARROWS_PER_CRAFT); - speak_with_history("Crafted " + ARROWS_PER_CRAFT + " arrows.", true); + dictionary arrowArgs; + arrowArgs.set("count", ARROWS_PER_CRAFT); + speak_with_history(trf("system.crafting.materials.crafted.arrows", arrowArgs), true); } else { - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); } } void craft_arrows_max() { if (get_personal_count(ITEM_QUIVERS) <= 0) { - speak_with_history("You need a quiver to craft arrows.", true); + speak_with_history(tr("system.inventory.need_quiver_for_arrows"), true); return; } @@ -96,7 +111,7 @@ void craft_arrows_max() { int capacity = get_arrow_limit() - currentArrows; int maxByCapacity = capacity / ARROWS_PER_CRAFT; if (maxByCapacity <= 0) { - speak_with_history("Not enough quiver capacity for arrows.", true); + speak_with_history(tr("system.crafting.materials.not_enough_quiver_capacity_for_arrows"), true); return; } @@ -104,16 +119,22 @@ void craft_arrows_max() { int maxByFeathers = get_personal_count(ITEM_FEATHERS) / 4; int maxByStones = get_personal_count(ITEM_STONES) / 2; int maxCraft = maxBySticks; - if (maxByFeathers < maxCraft) maxCraft = maxByFeathers; - if (maxByStones < maxCraft) maxCraft = maxByStones; - if (maxByCapacity < maxCraft) maxCraft = maxByCapacity; + if (maxByFeathers < maxCraft) + maxCraft = maxByFeathers; + if (maxByStones < maxCraft) + maxCraft = maxByStones; + if (maxByCapacity < maxCraft) + maxCraft = maxByCapacity; if (maxCraft <= 0) { string missing = ""; - if (get_personal_count(ITEM_STICKS) < 2) missing += "2 sticks "; - if (get_personal_count(ITEM_FEATHERS) < 4) missing += "4 feathers "; - if (get_personal_count(ITEM_STONES) < 2) missing += "2 stones "; - speak_with_history("Missing: " + missing, true); + if (get_personal_count(ITEM_STICKS) < 2) + missing += "2 sticks "; + if (get_personal_count(ITEM_FEATHERS) < 4) + missing += "4 feathers "; + if (get_personal_count(ITEM_STONES) < 2) + missing += "2 stones "; + speak_crafting_missing(missing); return; } @@ -124,42 +145,45 @@ void craft_arrows_max() { add_personal_count(ITEM_FEATHERS, -(maxCraft * 4)); add_personal_count(ITEM_STONES, -(maxCraft * 2)); add_personal_count(ITEM_ARROWS, ARROWS_PER_CRAFT * maxCraft); - speak_with_history("Crafted " + (ARROWS_PER_CRAFT * maxCraft) + " arrows.", true); + dictionary arrowsMaxArgs; + arrowsMaxArgs.set("count", ARROWS_PER_CRAFT * maxCraft); + speak_with_history(trf("system.crafting.materials.crafted.arrows", arrowsMaxArgs), true); } void craft_bowstring() { - WorldFire@ fire = get_fire_within_range(x, 3); + 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; } string missing = ""; - if (get_personal_count(ITEM_SINEW) < 3) missing += "3 sinew "; + if (get_personal_count(ITEM_SINEW) < 3) + missing += "3 sinew "; 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); add_personal_count(ITEM_SINEW, -3); add_personal_count(ITEM_BOWSTRINGS, 1); - speak_with_history("Crafted a bowstring.", true); + speak_with_history(tr("system.crafting.materials.crafted.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); + 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; } @@ -167,10 +191,11 @@ void craft_bowstring_max() { int max_craft = max_by_sinew; int space = get_personal_stack_limit() - get_personal_count(ITEM_BOWSTRINGS); - if (max_craft > space) max_craft = space; + if (max_craft > space) + max_craft = space; if (max_craft <= 0) { - speak_with_history("Missing: 3 sinew", true); + speak_crafting_missing("3 sinew"); return; } @@ -179,23 +204,28 @@ void craft_bowstring_max() { simulate_crafting(craft_time); add_personal_count(ITEM_SINEW, -(max_craft * 3)); add_personal_count(ITEM_BOWSTRINGS, max_craft); - speak_with_history("Crafted " + max_craft + " Bowstrings.", true); + dictionary bowstringArgs; + bowstringArgs.set("count", max_craft); + speak_with_history(trf("system.crafting.materials.crafted_max.bowstrings", bowstringArgs), true); } 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; } string missing = ""; - if (get_personal_count(ITEM_STICKS) < INCENSE_STICK_COST) missing += INCENSE_STICK_COST + " sticks "; - if (get_personal_count(ITEM_VINES) < INCENSE_VINE_COST) missing += INCENSE_VINE_COST + " vines "; - if (get_personal_count(ITEM_REEDS) < INCENSE_REED_COST) missing += INCENSE_REED_COST + " reed "; + if (get_personal_count(ITEM_STICKS) < INCENSE_STICK_COST) + missing += INCENSE_STICK_COST + " sticks "; + if (get_personal_count(ITEM_VINES) < INCENSE_VINE_COST) + missing += INCENSE_VINE_COST + " vines "; + if (get_personal_count(ITEM_REEDS) < INCENSE_REED_COST) + missing += INCENSE_REED_COST + " reed "; 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); @@ -203,20 +233,20 @@ void craft_incense() { add_personal_count(ITEM_VINES, -INCENSE_VINE_COST); add_personal_count(ITEM_REEDS, -INCENSE_REED_COST); add_personal_count(ITEM_INCENSE, 1); - speak_with_history("Crafted incense.", true); + speak_with_history(tr("system.crafting.materials.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; } @@ -224,47 +254,58 @@ void craft_incense_max() { int max_by_vines = get_personal_count(ITEM_VINES) / INCENSE_VINE_COST; int max_by_reeds = get_personal_count(ITEM_REEDS) / INCENSE_REED_COST; int max_craft = max_by_sticks; - if (max_by_vines < max_craft) max_craft = max_by_vines; - if (max_by_reeds < max_craft) max_craft = max_by_reeds; + if (max_by_vines < max_craft) + max_craft = max_by_vines; + if (max_by_reeds < max_craft) + max_craft = max_by_reeds; int space = get_personal_stack_limit() - get_personal_count(ITEM_INCENSE); - if (max_craft > space) max_craft = space; + if (max_craft > space) + max_craft = space; if (max_craft <= 0) { string missing = ""; - if (get_personal_count(ITEM_STICKS) < INCENSE_STICK_COST) missing += INCENSE_STICK_COST + " sticks "; - if (get_personal_count(ITEM_VINES) < INCENSE_VINE_COST) 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); + if (get_personal_count(ITEM_STICKS) < INCENSE_STICK_COST) + missing += INCENSE_STICK_COST + " sticks "; + if (get_personal_count(ITEM_VINES) < INCENSE_VINE_COST) + missing += INCENSE_VINE_COST + " vines "; + if (get_personal_count(ITEM_REEDS) < INCENSE_REED_COST) + missing += INCENSE_REED_COST + " reed "; + speak_crafting_missing(missing); return; } - int total_cost = (max_craft * INCENSE_STICK_COST) + (max_craft * INCENSE_VINE_COST) + (max_craft * INCENSE_REED_COST); + int total_cost = + (max_craft * INCENSE_STICK_COST) + (max_craft * INCENSE_VINE_COST) + (max_craft * INCENSE_REED_COST); simulate_crafting(total_cost); add_personal_count(ITEM_STICKS, -(max_craft * INCENSE_STICK_COST)); add_personal_count(ITEM_VINES, -(max_craft * INCENSE_VINE_COST)); add_personal_count(ITEM_REEDS, -(max_craft * INCENSE_REED_COST)); add_personal_count(ITEM_INCENSE, max_craft); - speak_with_history("Crafted " + max_craft + " Incense.", true); + dictionary incenseArgs; + incenseArgs.set("count", max_craft); + speak_with_history(trf("system.crafting.materials.crafted_max.incense", incenseArgs), true); } void craft_smoke_fish() { - WorldFire@ fire = get_fire_within_range(x, 3); + 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; } string missing = ""; - if (get_personal_count(ITEM_FISH) < 1) missing += "1 fish "; - if (get_personal_count(ITEM_STICKS) < 1) missing += "1 stick "; + if (get_personal_count(ITEM_FISH) < 1) + missing += "1 fish "; + if (get_personal_count(ITEM_STICKS) < 1) + missing += "1 stick "; if (missing == "") { int weight = (personal_fish_weights.length() > 0) ? personal_fish_weights[0] : get_default_fish_weight(); 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); @@ -272,35 +313,40 @@ void craft_smoke_fish() { add_personal_count(ITEM_FISH, -1); add_personal_count(ITEM_STICKS, -1); add_personal_count(ITEM_SMOKED_FISH, yield); - speak_with_history("Smoked a fish into " + yield + " smoked fish.", true); + dictionary smokeFishArgs; + smokeFishArgs.set("yield", yield); + speak_with_history(trf("system.crafting.materials.smoke_fish.crafted", smokeFishArgs), 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); + 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; } int max_by_fish = get_personal_count(ITEM_FISH); int max_by_sticks = get_personal_count(ITEM_STICKS); int max_craft = max_by_fish; - if (max_by_sticks < max_craft) max_craft = max_by_sticks; + if (max_by_sticks < max_craft) + max_craft = max_by_sticks; if (max_craft <= 0) { string missing = ""; - if (get_personal_count(ITEM_FISH) < 1) missing += "1 fish "; - if (get_personal_count(ITEM_STICKS) < 1) missing += "1 stick "; - speak_with_history("Missing: " + missing, true); + if (get_personal_count(ITEM_FISH) < 1) + missing += "1 fish "; + if (get_personal_count(ITEM_STICKS) < 1) + missing += "1 stick "; + 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; } @@ -309,13 +355,14 @@ void craft_smoke_fish_max() { for (int i = 0; i < max_craft; i++) { int weight = (i < int(personal_fish_weights.length())) ? personal_fish_weights[i] : get_default_fish_weight(); int yield = get_smoked_fish_yield(weight); - if (total_yield + yield > space) break; + if (total_yield + yield > space) + break; total_yield += yield; fish_to_smoke++; } 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; } @@ -328,32 +375,37 @@ void craft_smoke_fish_max() { add_personal_count(ITEM_FISH, -fish_to_smoke); add_personal_count(ITEM_STICKS, -fish_to_smoke); add_personal_count(ITEM_SMOKED_FISH, total_yield); - speak_with_history("Smoked " + fish_to_smoke + " fish into " + total_yield + " smoked fish.", true); + dictionary smokeFishMaxArgs; + smokeFishMaxArgs.set("count", fish_to_smoke); + smokeFishMaxArgs.set("yield", total_yield); + speak_with_history(trf("system.crafting.materials.smoke_fish.crafted_max", smokeFishMaxArgs), true); } void butcher_small_game() { string missing = ""; // Check for knife - if (get_personal_count(ITEM_KNIVES) < 1) missing += "Stone Knife "; + if (get_personal_count(ITEM_KNIVES) < 1) + missing += "Stone Knife "; // Check for small game or boar - if (get_personal_count(ITEM_SMALL_GAME) < 1 && get_personal_count(ITEM_BOAR_CARCASSES) < 1) missing += "Game "; + if (get_personal_count(ITEM_SMALL_GAME) < 1 && get_personal_count(ITEM_BOAR_CARCASSES) < 1) + missing += "Game "; // Check for fire within 3 tiles (can hear it) - WorldFire@ fire = get_fire_within_range(x, 3); + 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); @@ -372,26 +424,28 @@ void butcher_small_game() { add_personal_count(ITEM_MEAT, 1); add_personal_count(ITEM_FEATHERS, random(3, 6)); add_personal_count(ITEM_DOWN, random(1, 3)); - speak_with_history("Butchered goose. Got 1 meat, feathers, and down.", true); + speak_with_history(tr("system.crafting.materials.butcher.goose"), true); } else if (game_type == "turkey") { add_personal_count(ITEM_MEAT, 1); add_personal_count(ITEM_FEATHERS, random(1, 4)); - speak_with_history("Butchered turkey. Got 1 meat and feathers.", true); + speak_with_history(tr("system.crafting.materials.butcher.turkey"), true); } else if (game_type == "boar carcass") { add_personal_count(ITEM_MEAT, random(2, 3)); add_personal_count(ITEM_SKINS, 3); add_personal_count(ITEM_SINEW, 2); - speak_with_history("Butchered boar. Got meat, 3 skins, and 2 sinew.", true); + speak_with_history(tr("system.crafting.materials.butcher.boar"), true); } else { add_personal_count(ITEM_MEAT, 1); add_personal_count(ITEM_SKINS, 1); - speak_with_history("Butchered " + game_type + ". Got 1 meat and 1 skin.", true); + dictionary butcherDefaultArgs; + butcherDefaultArgs.set("game", i18n_translate_fragment_value(game_type)); + speak_with_history(trf("system.crafting.materials.butcher.default", butcherDefaultArgs), true); } // Play sound p.play_stationary("sounds/items/miscellaneous.ogg", false); } else { - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); } } @@ -400,20 +454,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); + 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; } @@ -422,11 +476,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; } @@ -435,11 +489,13 @@ void butcher_small_game_max() { // Determine limiting factor int max_craft = total_game; - if (meat_space < max_craft) max_craft = meat_space; - if (skins_space < max_craft) max_craft = skins_space; + if (meat_space < max_craft) + max_craft = meat_space; + if (skins_space < max_craft) + max_craft = skins_space; if (max_craft <= 0) { - speak_with_history("No space for outputs.", true); + speak_with_history(tr("system.crafting.materials.butcher.no_space_outputs"), true); return; } @@ -468,8 +524,10 @@ void butcher_small_game_max() { game_type = personal_small_game_types[0]; personal_small_game_types.remove_at(0); add_personal_count(ITEM_SMALL_GAME, -1); - if (game_type == "goose") geese_count++; - if (game_type == "turkey") turkey_count++; + if (game_type == "goose") + geese_count++; + if (game_type == "turkey") + turkey_count++; } if (game_type == "goose") { @@ -496,14 +554,13 @@ void butcher_small_game_max() { add_personal_count(ITEM_DOWN, total_down); add_personal_count(ITEM_SINEW, total_sinew); - // Build result message - string result = "Butchered " + max_craft + " game. Got " + total_meat + " meat"; - if (total_skins > 0) result += ", " + total_skins + " skins"; - if (total_feathers > 0) result += ", feathers"; - if (total_down > 0) result += ", and down"; - if (total_sinew > 0) result += ", and " + total_sinew + " sinew"; - result += "."; - - speak_with_history(result, true); + dictionary butcherMaxArgs; + butcherMaxArgs.set("count", max_craft); + butcherMaxArgs.set("meat", total_meat); + butcherMaxArgs.set("skins", total_skins); + butcherMaxArgs.set("feathers", total_feathers); + butcherMaxArgs.set("down", total_down); + butcherMaxArgs.set("sinew", total_sinew); + speak_with_history(trf("system.crafting.materials.butcher.max_result", butcherMaxArgs), true); p.play_stationary("sounds/items/miscellaneous.ogg", false); } diff --git a/src/crafting/craft_runes.nvgt b/src/crafting/craft_runes.nvgt index b88e593..324e4cf 100644 --- a/src/crafting/craft_runes.nvgt +++ b/src/crafting/craft_runes.nvgt @@ -4,74 +4,146 @@ // Get the base equipment name without any rune prefix string get_base_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"; + if (equip_type == EQUIP_SPEAR) + return i18n_lookup_key_with_fallback("system.equipment.name.spear", "Spear"); + if (equip_type == EQUIP_AXE) + return i18n_lookup_key_with_fallback("system.equipment.name.stone_axe", "Stone Axe"); + if (equip_type == EQUIP_SLING) + return i18n_lookup_key_with_fallback("system.equipment.name.sling", "Sling"); + if (equip_type == EQUIP_BOW) + return i18n_lookup_key_with_fallback("system.equipment.name.bow", "Bow"); + if (equip_type == EQUIP_HAT) + return i18n_lookup_key_with_fallback("system.equipment.name.skin_hat", "Skin Hat"); + if (equip_type == EQUIP_GLOVES) + return i18n_lookup_key_with_fallback("system.equipment.name.skin_gloves", "Skin Gloves"); + if (equip_type == EQUIP_PANTS) + return i18n_lookup_key_with_fallback("system.equipment.name.skin_pants", "Skin Pants"); + if (equip_type == EQUIP_TUNIC) + return i18n_lookup_key_with_fallback("system.equipment.name.skin_tunic", "Skin Tunic"); + if (equip_type == EQUIP_MOCCASINS) + return i18n_lookup_key_with_fallback("system.equipment.name.moccasins", "Moccasins"); + if (equip_type == EQUIP_POUCH) + return i18n_lookup_key_with_fallback("system.equipment.name.skin_pouch", "Skin Pouch"); + if (equip_type == EQUIP_BACKPACK) + return i18n_lookup_key_with_fallback("system.equipment.name.backpack", "Backpack"); + if (equip_type == EQUIP_FISHING_POLE) + 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"; - if (equip_type == EQUIP_AXE) return "Stone Axes"; - if (equip_type == EQUIP_SLING) return "Slings"; - if (equip_type == EQUIP_BOW) return "Bows"; - if (equip_type == EQUIP_HAT) return "Skin Hats"; - if (equip_type == EQUIP_GLOVES) return "Skin Gloves"; - if (equip_type == EQUIP_PANTS) return "Skin Pants"; - if (equip_type == EQUIP_TUNIC) return "Skin Tunics"; - if (equip_type == EQUIP_MOCCASINS) return "Moccasins"; - if (equip_type == EQUIP_POUCH) return "Skin Pouches"; - if (equip_type == EQUIP_BACKPACK) return "Backpacks"; - if (equip_type == EQUIP_FISHING_POLE) return "Fishing Poles"; - return "Items"; + if (equip_type == EQUIP_SPEAR) + return i18n_lookup_key_with_fallback("system.equipment.name_plural.spears", "Spears"); + if (equip_type == EQUIP_AXE) + return i18n_lookup_key_with_fallback("system.equipment.name_plural.stone_axes", "Stone Axes"); + if (equip_type == EQUIP_SLING) + return i18n_lookup_key_with_fallback("system.equipment.name_plural.slings", "Slings"); + if (equip_type == EQUIP_BOW) + return i18n_lookup_key_with_fallback("system.equipment.name_plural.bows", "Bows"); + if (equip_type == EQUIP_HAT) + return i18n_lookup_key_with_fallback("system.equipment.name_plural.skin_hats", "Skin Hats"); + if (equip_type == EQUIP_GLOVES) + return i18n_lookup_key_with_fallback("system.equipment.name_plural.skin_gloves", "Skin Gloves"); + if (equip_type == EQUIP_PANTS) + return i18n_lookup_key_with_fallback("system.equipment.name_plural.skin_pants", "Skin Pants"); + if (equip_type == EQUIP_TUNIC) + return i18n_lookup_key_with_fallback("system.equipment.name_plural.skin_tunics", "Skin Tunics"); + if (equip_type == EQUIP_MOCCASINS) + return i18n_lookup_key_with_fallback("system.equipment.name_plural.moccasins", "Moccasins"); + if (equip_type == EQUIP_POUCH) + return i18n_lookup_key_with_fallback("system.equipment.name_plural.skin_pouches", "Skin Pouches"); + if (equip_type == EQUIP_BACKPACK) + return i18n_lookup_key_with_fallback("system.equipment.name_plural.backpacks", "Backpacks"); + if (equip_type == EQUIP_FISHING_POLE) + 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 int get_unruned_equipment_count(int equip_type) { - if (equip_type == EQUIP_SPEAR) return get_personal_count(ITEM_SPEARS); - if (equip_type == EQUIP_AXE) return get_personal_count(ITEM_AXES); - if (equip_type == EQUIP_SLING) return get_personal_count(ITEM_SLINGS); - if (equip_type == EQUIP_BOW) return get_personal_count(ITEM_BOWS); - if (equip_type == EQUIP_HAT) return get_personal_count(ITEM_SKIN_HATS); - if (equip_type == EQUIP_GLOVES) return get_personal_count(ITEM_SKIN_GLOVES); - if (equip_type == EQUIP_PANTS) return get_personal_count(ITEM_SKIN_PANTS); - if (equip_type == EQUIP_TUNIC) return get_personal_count(ITEM_SKIN_TUNICS); - if (equip_type == EQUIP_MOCCASINS) return get_personal_count(ITEM_MOCCASINS); - if (equip_type == EQUIP_POUCH) return get_personal_count(ITEM_SKIN_POUCHES); - if (equip_type == EQUIP_BACKPACK) return get_personal_count(ITEM_BACKPACKS); - if (equip_type == EQUIP_FISHING_POLE) return get_personal_count(ITEM_FISHING_POLES); + if (equip_type == EQUIP_SPEAR) + return get_personal_count(ITEM_SPEARS); + if (equip_type == EQUIP_AXE) + return get_personal_count(ITEM_AXES); + if (equip_type == EQUIP_SLING) + return get_personal_count(ITEM_SLINGS); + if (equip_type == EQUIP_BOW) + return get_personal_count(ITEM_BOWS); + if (equip_type == EQUIP_HAT) + return get_personal_count(ITEM_SKIN_HATS); + if (equip_type == EQUIP_GLOVES) + return get_personal_count(ITEM_SKIN_GLOVES); + if (equip_type == EQUIP_PANTS) + return get_personal_count(ITEM_SKIN_PANTS); + if (equip_type == EQUIP_TUNIC) + return get_personal_count(ITEM_SKIN_TUNICS); + if (equip_type == EQUIP_MOCCASINS) + return get_personal_count(ITEM_MOCCASINS); + if (equip_type == EQUIP_POUCH) + return get_personal_count(ITEM_SKIN_POUCHES); + if (equip_type == EQUIP_BACKPACK) + return get_personal_count(ITEM_BACKPACKS); + if (equip_type == EQUIP_FISHING_POLE) + return get_personal_count(ITEM_FISHING_POLES); return 0; } // Decrement inventory for an equipment type void decrement_unruned_equipment(int equip_type) { - if (equip_type == EQUIP_SPEAR) { add_personal_count(ITEM_SPEARS, -1); return; } - if (equip_type == EQUIP_AXE) { add_personal_count(ITEM_AXES, -1); return; } - if (equip_type == EQUIP_SLING) { add_personal_count(ITEM_SLINGS, -1); return; } - if (equip_type == EQUIP_BOW) { add_personal_count(ITEM_BOWS, -1); return; } - if (equip_type == EQUIP_HAT) { add_personal_count(ITEM_SKIN_HATS, -1); return; } - if (equip_type == EQUIP_GLOVES) { add_personal_count(ITEM_SKIN_GLOVES, -1); return; } - if (equip_type == EQUIP_PANTS) { add_personal_count(ITEM_SKIN_PANTS, -1); return; } - if (equip_type == EQUIP_TUNIC) { add_personal_count(ITEM_SKIN_TUNICS, -1); return; } - if (equip_type == EQUIP_MOCCASINS) { add_personal_count(ITEM_MOCCASINS, -1); return; } - if (equip_type == EQUIP_POUCH) { add_personal_count(ITEM_SKIN_POUCHES, -1); return; } - if (equip_type == EQUIP_BACKPACK) { add_personal_count(ITEM_BACKPACKS, -1); return; } - if (equip_type == EQUIP_FISHING_POLE) { add_personal_count(ITEM_FISHING_POLES, -1); return; } + if (equip_type == EQUIP_SPEAR) { + add_personal_count(ITEM_SPEARS, -1); + return; + } + if (equip_type == EQUIP_AXE) { + add_personal_count(ITEM_AXES, -1); + return; + } + if (equip_type == EQUIP_SLING) { + add_personal_count(ITEM_SLINGS, -1); + return; + } + if (equip_type == EQUIP_BOW) { + add_personal_count(ITEM_BOWS, -1); + return; + } + if (equip_type == EQUIP_HAT) { + add_personal_count(ITEM_SKIN_HATS, -1); + return; + } + if (equip_type == EQUIP_GLOVES) { + add_personal_count(ITEM_SKIN_GLOVES, -1); + return; + } + if (equip_type == EQUIP_PANTS) { + add_personal_count(ITEM_SKIN_PANTS, -1); + return; + } + if (equip_type == EQUIP_TUNIC) { + add_personal_count(ITEM_SKIN_TUNICS, -1); + return; + } + if (equip_type == EQUIP_MOCCASINS) { + add_personal_count(ITEM_MOCCASINS, -1); + return; + } + if (equip_type == EQUIP_POUCH) { + add_personal_count(ITEM_SKIN_POUCHES, -1); + return; + } + if (equip_type == EQUIP_BACKPACK) { + add_personal_count(ITEM_BACKPACKS, -1); + return; + } + if (equip_type == EQUIP_FISHING_POLE) { + add_personal_count(ITEM_FISHING_POLES, -1); + return; + } } void run_runes_menu() { // Check if in base area if (x > BASE_END) { - speak_with_history("Rune engraving can only be done in the base area.", true); + speak_with_history(tr("system.crafting.runes.base_only"), true); return; } @@ -82,40 +154,44 @@ void run_runes_menu() { get_unlocked_rune_types(unlocked_runes); for (uint i = 0; i < unlocked_runes.length(); i++) { int rune_type = unlocked_runes[i]; - string label = get_rune_name(rune_type) + " (1 Clay, 1 Favor) [Requires Knife]"; + dictionary runeOptionArgs; + runeOptionArgs.set("rune", get_rune_name(rune_type)); + string label = trf("system.crafting.runes.option", runeOptionArgs); rune_options.insert_last(label); rune_types.insert_last(rune_type); } if (rune_options.length() == 0) { - speak_with_history("No runes unlocked yet.", true); + speak_with_history(tr("system.crafting.runes.none_unlocked"), true); return; } int selection = 0; - speak_with_history("Runes. " + rune_options[selection], true); + speak_menu_prompt("system.crafting.runes.prompt", rune_options[selection]); - while(true) { + while (true) { wait(5); if (menu_background_tick()) { return; } if (key_pressed(KEY_ESCAPE)) { - speak_with_history("Closed.", true); + speak_with_history(tr("system.menu.closed"), true); break; } if (key_pressed(KEY_DOWN)) { play_menu_move_sound(); selection++; - if (selection >= int(rune_options.length())) selection = 0; + if (selection >= int(rune_options.length())) + selection = 0; speak_with_history(rune_options[selection], true); } if (key_pressed(KEY_UP)) { play_menu_move_sound(); selection--; - if (selection < 0) selection = int(rune_options.length()) - 1; + if (selection < 0) + selection = int(rune_options.length()) - 1; speak_with_history(rune_options[selection], true); } @@ -139,40 +215,48 @@ 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)"); + dictionary equipmentOptionArgs; + equipmentOptionArgs.set("name", name); + equipmentOptionArgs.set("count", unruned_count); + equipment_options.insert_last(trf("system.crafting.runes.equipment_option", equipmentOptionArgs)); equipment_types.insert_last(equip_type); } } if (equipment_options.length() == 0) { - speak_with_history("No equipment available to engrave.", true); + speak_with_history(tr("system.crafting.runes.no_equipment_available"), true); return; } int selection = 0; - speak_with_history("Select equipment to engrave with " + get_rune_name(rune_type) + ". " + equipment_options[selection], true); + dictionary equipmentPromptArgs; + equipmentPromptArgs.set("rune", get_rune_name(rune_type)); + equipmentPromptArgs.set("option", equipment_options[selection]); + speak_with_history(trf("system.crafting.runes.select_equipment_prompt", equipmentPromptArgs), true); - while(true) { + while (true) { wait(5); if (menu_background_tick()) { return; } if (key_pressed(KEY_ESCAPE)) { - speak_with_history("Closed.", true); + speak_with_history(tr("system.menu.closed"), true); break; } if (key_pressed(KEY_DOWN)) { play_menu_move_sound(); selection++; - if (selection >= int(equipment_options.length())) selection = 0; + if (selection >= int(equipment_options.length())) + selection = 0; speak_with_history(equipment_options[selection], true); } if (key_pressed(KEY_UP)) { play_menu_move_sound(); selection--; - if (selection < 0) selection = int(equipment_options.length()) - 1; + if (selection < 0) + selection = int(equipment_options.length()) - 1; speak_with_history(equipment_options[selection], true); } @@ -195,13 +279,18 @@ void run_rune_equipment_menu(int rune_type) { void engrave_rune(int equip_type, int rune_type) { // Validate requirements string missing = ""; - if (get_personal_count(ITEM_KNIVES) < 1) missing += "Stone Knife "; - if (get_personal_count(ITEM_CLAY) < 1) missing += "1 clay "; - if (favor < 1.0) missing += "1 favor "; + if (get_personal_count(ITEM_KNIVES) < 1) + missing += "Stone Knife "; + if (get_personal_count(ITEM_CLAY) < 1) + missing += "1 clay "; + if (favor < 1.0) + missing += "1 favor "; // Check equipment is still available if (get_unruned_equipment_count(equip_type) < 1) { - speak_with_history("No " + get_base_equipment_name(equip_type) + " available.", true); + dictionary noItemArgs; + noItemArgs.set("item", get_base_equipment_name(equip_type)); + speak_with_history(trf("system.crafting.runes.no_item_available", noItemArgs), true); return; } @@ -219,40 +308,54 @@ void engrave_rune(int equip_type, int rune_type) { // Play crafting animation simulate_crafting(6); - string runed_name = "Runed " + get_base_equipment_name(equip_type) + " of " + get_rune_effect_name(rune_type); - speak_with_history("Engraved " + runed_name + ".", true); + dictionary runedNameArgs; + runedNameArgs.set("equipment", get_base_equipment_name(equip_type)); + runedNameArgs.set("rune", get_rune_effect_name(rune_type)); + string runed_name = trf("system.crafting.runes.runed_name", runedNameArgs); + + dictionary engravedArgs; + engravedArgs.set("item", runed_name); + speak_with_history(trf("system.crafting.runes.engraved", engravedArgs), true); } else { - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); } } void engrave_rune_max(int equip_type, int rune_type) { // Validate requirements string missing = ""; - if (get_personal_count(ITEM_KNIVES) < 1) missing += "Stone Knife "; + if (get_personal_count(ITEM_KNIVES) < 1) + missing += "Stone Knife "; int unruned_count = get_unruned_equipment_count(equip_type); if (unruned_count < 1) { - speak_with_history("No " + get_base_equipment_name(equip_type) + " available.", true); + dictionary noItemArgs; + noItemArgs.set("item", get_base_equipment_name(equip_type)); + speak_with_history(trf("system.crafting.runes.no_item_available", noItemArgs), true); return; } int clay_count = get_personal_count(ITEM_CLAY); int favor_count = int(favor); int max_craft = unruned_count; - if (clay_count < max_craft) max_craft = clay_count; - if (favor_count < max_craft) max_craft = favor_count; + if (clay_count < max_craft) + max_craft = clay_count; + if (favor_count < max_craft) + max_craft = favor_count; if (missing != "") { - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); return; } if (max_craft <= 0) { - if (clay_count < 1) missing += "1 clay "; - if (favor < 1.0) missing += "1 favor "; - if (missing == "") missing = "resources"; - speak_with_history("Missing: " + missing, true); + if (clay_count < 1) + missing += "1 clay "; + if (favor < 1.0) + missing += "1 favor "; + if (missing == "") + missing = "resources"; + speak_crafting_missing(missing); return; } @@ -267,6 +370,11 @@ void engrave_rune_max(int equip_type, int rune_type) { simulate_crafting(6 * max_craft); - string item_name = (max_craft == 1) ? get_base_equipment_name(equip_type) : get_base_equipment_name_plural(equip_type); - speak_with_history("Engraved " + max_craft + " " + item_name + " with " + get_rune_name(rune_type) + ".", true); + string item_name = + (max_craft == 1) ? get_base_equipment_name(equip_type) : get_base_equipment_name_plural(equip_type); + dictionary engravedMaxArgs; + engravedMaxArgs.set("count", max_craft); + engravedMaxArgs.set("item", item_name); + engravedMaxArgs.set("rune", get_rune_name(rune_type)); + speak_with_history(trf("system.crafting.runes.engraved_max", engravedMaxArgs), true); } diff --git a/src/crafting/craft_tools.nvgt b/src/crafting/craft_tools.nvgt index 7b77873..f51a62b 100644 --- a/src/crafting/craft_tools.nvgt +++ b/src/crafting/craft_tools.nvgt @@ -1,68 +1,86 @@ // 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) { + while (true) { wait(5); if (menu_background_tick()) { return; } if (key_pressed(KEY_ESCAPE)) { - speak_with_history("Closed.", true); + speak_with_history(tr("system.menu.closed"), true); break; } 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); } if (key_pressed(KEY_RETURN)) { play_menu_select_sound(); - if (selection == 0) craft_knife(); - else if (selection == 1) craft_snare(); - else if (selection == 2) craft_axe(); - else if (selection == 3) craft_fishing_pole(); - else if (selection == 4) craft_rope(); - else if (selection == 5) craft_quiver(); - else if (selection == 6) craft_canoe(); - else if (selection == 7) craft_reed_basket(); - else if (selection == 8) craft_clay_pot(); + if (selection == 0) + craft_knife(); + else if (selection == 1) + craft_snare(); + else if (selection == 2) + craft_axe(); + else if (selection == 3) + craft_fishing_pole(); + else if (selection == 4) + craft_rope(); + else if (selection == 5) + craft_quiver(); + else if (selection == 6) + craft_canoe(); + else if (selection == 7) + craft_reed_basket(); + else if (selection == 8) + craft_clay_pot(); break; } if (key_pressed(KEY_TAB)) { play_menu_select_sound(); - if (selection == 0) craft_knife_max(); - else if (selection == 1) craft_snare_max(); - else if (selection == 2) craft_axe_max(); - else if (selection == 3) craft_fishing_pole_max(); - else if (selection == 4) craft_rope_max(); - else if (selection == 5) craft_quiver_max(); - else if (selection == 6) craft_canoe_max(); - else if (selection == 7) craft_reed_basket_max(); - else if (selection == 8) craft_clay_pot_max(); + if (selection == 0) + craft_knife_max(); + else if (selection == 1) + craft_snare_max(); + else if (selection == 2) + craft_axe_max(); + else if (selection == 3) + craft_fishing_pole_max(); + else if (selection == 4) + craft_rope_max(); + else if (selection == 5) + craft_quiver_max(); + else if (selection == 6) + craft_canoe_max(); + else if (selection == 7) + craft_reed_basket_max(); + else if (selection == 8) + craft_clay_pot_max(); break; } } @@ -70,34 +88,36 @@ void run_tools_menu() { void craft_knife() { string missing = ""; - if (get_personal_count(ITEM_STONES) < 2) missing += "2 stones "; + if (get_personal_count(ITEM_STONES) < 2) + missing += "2 stones "; 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); add_personal_count(ITEM_STONES, -2); add_personal_count(ITEM_KNIVES, 1); - speak_with_history("Crafted a Stone Knife.", true); + speak_with_history(tr("system.crafting.tools.crafted.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; } int max_possible = get_personal_count(ITEM_STONES) / 2; int space = get_personal_stack_limit() - get_personal_count(ITEM_KNIVES); - if (max_possible > space) max_possible = space; + if (max_possible > space) + max_possible = space; if (max_possible <= 0) { - speak_with_history("Missing: 2 stones", true); + speak_crafting_missing("2 stones"); return; } @@ -106,48 +126,56 @@ void craft_knife_max() { simulate_crafting(craft_time); add_personal_count(ITEM_STONES, -(max_possible * 2)); add_personal_count(ITEM_KNIVES, max_possible); - speak_with_history("Crafted " + max_possible + " Stone Knives.", true); + dictionary knifeArgs; + knifeArgs.set("count", max_possible); + speak_with_history(trf("system.crafting.tools.crafted_max.stone_knives", knifeArgs), true); } void craft_snare() { string missing = ""; - if (get_personal_count(ITEM_STICKS) < 1) missing += "1 stick "; - if (get_personal_count(ITEM_VINES) < 2) missing += "2 vines "; + if (get_personal_count(ITEM_STICKS) < 1) + missing += "1 stick "; + if (get_personal_count(ITEM_VINES) < 2) + missing += "2 vines "; 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); add_personal_count(ITEM_STICKS, -1); add_personal_count(ITEM_VINES, -2); add_personal_count(ITEM_SNARES, 1); - speak_with_history("Crafted a Snare.", true); + speak_with_history(tr("system.crafting.tools.crafted.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; } int max_by_sticks = get_personal_count(ITEM_STICKS); int max_by_vines = get_personal_count(ITEM_VINES) / 2; int max_craft = max_by_sticks; - if (max_by_vines < max_craft) max_craft = max_by_vines; + if (max_by_vines < max_craft) + max_craft = max_by_vines; int space = get_personal_stack_limit() - get_personal_count(ITEM_SNARES); - if (max_craft > space) max_craft = space; + if (max_craft > space) + max_craft = space; if (max_craft <= 0) { string missing = ""; - if (get_personal_count(ITEM_STICKS) < 1) missing += "1 stick "; - if (get_personal_count(ITEM_VINES) < 2) missing += "2 vines "; - speak_with_history("Missing: " + missing, true); + if (get_personal_count(ITEM_STICKS) < 1) + missing += "1 stick "; + if (get_personal_count(ITEM_VINES) < 2) + missing += "2 vines "; + speak_crafting_missing(missing); return; } @@ -157,48 +185,56 @@ void craft_snare_max() { add_personal_count(ITEM_STICKS, -max_craft); add_personal_count(ITEM_VINES, -(max_craft * 2)); add_personal_count(ITEM_SNARES, max_craft); - speak_with_history("Crafted " + max_craft + " Snares.", true); + dictionary snareArgs; + snareArgs.set("count", max_craft); + speak_with_history(trf("system.crafting.tools.crafted_max.snares", snareArgs), true); } void craft_fishing_pole() { string missing = ""; - if (get_personal_count(ITEM_STICKS) < 1) missing += "1 stick "; - if (get_personal_count(ITEM_VINES) < 2) missing += "2 vines "; + if (get_personal_count(ITEM_STICKS) < 1) + missing += "1 stick "; + if (get_personal_count(ITEM_VINES) < 2) + missing += "2 vines "; 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); add_personal_count(ITEM_STICKS, -1); add_personal_count(ITEM_VINES, -2); add_personal_count(ITEM_FISHING_POLES, 1); - speak_with_history("Crafted a Fishing Pole.", true); + speak_with_history(tr("system.crafting.tools.crafted.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; } int max_by_sticks = get_personal_count(ITEM_STICKS); int max_by_vines = get_personal_count(ITEM_VINES) / 2; int max_craft = max_by_sticks; - if (max_by_vines < max_craft) max_craft = max_by_vines; + if (max_by_vines < max_craft) + max_craft = max_by_vines; int space = get_personal_stack_limit() - get_personal_count(ITEM_FISHING_POLES); - if (max_craft > space) max_craft = space; + if (max_craft > space) + max_craft = space; if (max_craft <= 0) { string missing = ""; - if (get_personal_count(ITEM_STICKS) < 1) missing += "1 stick "; - if (get_personal_count(ITEM_VINES) < 2) missing += "2 vines "; - speak_with_history("Missing: " + missing, true); + if (get_personal_count(ITEM_STICKS) < 1) + missing += "1 stick "; + if (get_personal_count(ITEM_VINES) < 2) + missing += "2 vines "; + speak_crafting_missing(missing); return; } @@ -208,40 +244,44 @@ void craft_fishing_pole_max() { add_personal_count(ITEM_STICKS, -max_craft); add_personal_count(ITEM_VINES, -(max_craft * 2)); add_personal_count(ITEM_FISHING_POLES, max_craft); - speak_with_history("Crafted " + max_craft + " Fishing Poles.", true); + dictionary fishingPoleArgs; + fishingPoleArgs.set("count", max_craft); + speak_with_history(trf("system.crafting.tools.crafted_max.fishing_poles", fishingPoleArgs), true); } void craft_rope() { string missing = ""; - if (get_personal_count(ITEM_VINES) < 3) missing += "3 vines "; + if (get_personal_count(ITEM_VINES) < 3) + missing += "3 vines "; 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); add_personal_count(ITEM_VINES, -3); add_personal_count(ITEM_ROPES, 1); - speak_with_history("Crafted rope.", true); + speak_with_history(tr("system.crafting.tools.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; } int max_craft = get_personal_count(ITEM_VINES) / 3; int space = get_personal_stack_limit() - get_personal_count(ITEM_ROPES); - if (max_craft > space) max_craft = space; + if (max_craft > space) + max_craft = space; if (max_craft <= 0) { - speak_with_history("Missing: 3 vines", true); + speak_crafting_missing("3 vines"); return; } @@ -250,48 +290,56 @@ void craft_rope_max() { simulate_crafting(craft_time); add_personal_count(ITEM_VINES, -(max_craft * 3)); add_personal_count(ITEM_ROPES, max_craft); - speak_with_history("Crafted " + max_craft + " Rope.", true); + dictionary ropeArgs; + ropeArgs.set("count", max_craft); + speak_with_history(trf("system.crafting.tools.crafted_max.ropes", ropeArgs), true); } void craft_quiver() { string missing = ""; - if (get_personal_count(ITEM_SKINS) < 2) missing += "2 skins "; - if (get_personal_count(ITEM_VINES) < 2) missing += "2 vines "; + if (get_personal_count(ITEM_SKINS) < 2) + missing += "2 skins "; + if (get_personal_count(ITEM_VINES) < 2) + missing += "2 vines "; 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); add_personal_count(ITEM_SKINS, -2); add_personal_count(ITEM_VINES, -2); add_personal_count(ITEM_QUIVERS, 1); - speak_with_history("Crafted a Quiver.", true); + speak_with_history(tr("system.crafting.tools.crafted.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; } int maxBySkins = get_personal_count(ITEM_SKINS) / 2; int maxByVines = get_personal_count(ITEM_VINES) / 2; int maxCraft = maxBySkins; - if (maxByVines < maxCraft) maxCraft = maxByVines; + if (maxByVines < maxCraft) + maxCraft = maxByVines; int space = get_personal_stack_limit() - get_personal_count(ITEM_QUIVERS); - if (maxCraft > space) maxCraft = space; + if (maxCraft > space) + maxCraft = space; if (maxCraft <= 0) { string missing = ""; - if (get_personal_count(ITEM_SKINS) < 2) missing += "2 skins "; - if (get_personal_count(ITEM_VINES) < 2) missing += "2 vines "; - speak_with_history("Missing: " + missing, true); + if (get_personal_count(ITEM_SKINS) < 2) + missing += "2 skins "; + if (get_personal_count(ITEM_VINES) < 2) + missing += "2 vines "; + speak_crafting_missing(missing); return; } @@ -301,21 +349,29 @@ void craft_quiver_max() { add_personal_count(ITEM_SKINS, -(maxCraft * 2)); add_personal_count(ITEM_VINES, -(maxCraft * 2)); add_personal_count(ITEM_QUIVERS, maxCraft); - speak_with_history("Crafted " + maxCraft + " Quivers.", true); + dictionary quiverArgs; + quiverArgs.set("count", maxCraft); + speak_with_history(trf("system.crafting.tools.crafted_max.quivers", quiverArgs), true); } void craft_canoe() { string missing = ""; - if (get_personal_count(ITEM_LOGS) < 4) missing += "4 logs "; - if (get_personal_count(ITEM_STICKS) < 11) missing += "11 sticks "; - if (get_personal_count(ITEM_VINES) < 11) missing += "11 vines "; - if (get_personal_count(ITEM_SKINS) < 6) missing += "6 skins "; - if (get_personal_count(ITEM_ROPES) < 2) missing += "2 rope "; - if (get_personal_count(ITEM_REEDS) < 6) missing += "6 reeds "; + if (get_personal_count(ITEM_LOGS) < 4) + missing += "4 logs "; + if (get_personal_count(ITEM_STICKS) < 11) + missing += "11 sticks "; + if (get_personal_count(ITEM_VINES) < 11) + missing += "11 vines "; + if (get_personal_count(ITEM_SKINS) < 6) + missing += "6 skins "; + if (get_personal_count(ITEM_ROPES) < 2) + missing += "2 rope "; + if (get_personal_count(ITEM_REEDS) < 6) + missing += "6 reeds "; 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); @@ -326,15 +382,15 @@ void craft_canoe() { add_personal_count(ITEM_ROPES, -2); add_personal_count(ITEM_REEDS, -6); add_personal_count(ITEM_CANOES, 1); - speak_with_history("Crafted a Canoe.", true); + speak_with_history(tr("system.crafting.tools.crafted.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; } @@ -345,24 +401,36 @@ void craft_canoe_max() { int maxByRopes = get_personal_count(ITEM_ROPES) / 2; int maxByReeds = get_personal_count(ITEM_REEDS) / 6; int maxCraft = maxByLogs; - if (maxBySticks < maxCraft) maxCraft = maxBySticks; - if (maxByVines < maxCraft) maxCraft = maxByVines; - if (maxBySkins < maxCraft) maxCraft = maxBySkins; - if (maxByRopes < maxCraft) maxCraft = maxByRopes; - if (maxByReeds < maxCraft) maxCraft = maxByReeds; + if (maxBySticks < maxCraft) + maxCraft = maxBySticks; + if (maxByVines < maxCraft) + maxCraft = maxByVines; + if (maxBySkins < maxCraft) + maxCraft = maxBySkins; + if (maxByRopes < maxCraft) + maxCraft = maxByRopes; + if (maxByReeds < maxCraft) + maxCraft = maxByReeds; int space = get_personal_stack_limit() - get_personal_count(ITEM_CANOES); - if (maxCraft > space) maxCraft = space; + if (maxCraft > space) + maxCraft = space; if (maxCraft <= 0) { string missing = ""; - if (get_personal_count(ITEM_LOGS) < 4) missing += "4 logs "; - if (get_personal_count(ITEM_STICKS) < 11) missing += "11 sticks "; - if (get_personal_count(ITEM_VINES) < 11) missing += "11 vines "; - if (get_personal_count(ITEM_SKINS) < 6) missing += "6 skins "; - if (get_personal_count(ITEM_ROPES) < 2) missing += "2 rope "; - if (get_personal_count(ITEM_REEDS) < 6) missing += "6 reeds "; - speak_with_history("Missing: " + missing, true); + if (get_personal_count(ITEM_LOGS) < 4) + missing += "4 logs "; + if (get_personal_count(ITEM_STICKS) < 11) + missing += "11 sticks "; + if (get_personal_count(ITEM_VINES) < 11) + missing += "11 vines "; + if (get_personal_count(ITEM_SKINS) < 6) + missing += "6 skins "; + if (get_personal_count(ITEM_ROPES) < 2) + missing += "2 rope "; + if (get_personal_count(ITEM_REEDS) < 6) + missing += "6 reeds "; + speak_crafting_missing(missing); return; } @@ -376,40 +444,44 @@ void craft_canoe_max() { add_personal_count(ITEM_ROPES, -(maxCraft * 2)); add_personal_count(ITEM_REEDS, -(maxCraft * 6)); add_personal_count(ITEM_CANOES, maxCraft); - speak_with_history("Crafted " + maxCraft + " Canoes.", true); + dictionary canoeArgs; + canoeArgs.set("count", maxCraft); + speak_with_history(trf("system.crafting.tools.crafted_max.canoes", canoeArgs), true); } void craft_reed_basket() { string missing = ""; - if (get_personal_count(ITEM_REEDS) < 3) missing += "3 reeds "; + if (get_personal_count(ITEM_REEDS) < 3) + missing += "3 reeds "; 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); add_personal_count(ITEM_REEDS, -3); add_personal_count(ITEM_REED_BASKETS, 1); - speak_with_history("Crafted a reed basket.", true); + speak_with_history(tr("system.crafting.tools.crafted.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; } int max_craft = get_personal_count(ITEM_REEDS) / 3; int space = get_personal_stack_limit() - get_personal_count(ITEM_REED_BASKETS); - if (max_craft > space) max_craft = space; + if (max_craft > space) + max_craft = space; if (max_craft <= 0) { - speak_with_history("Missing: 3 reeds", true); + speak_crafting_missing("3 reeds"); return; } @@ -418,54 +490,58 @@ void craft_reed_basket_max() { simulate_crafting(craft_time); add_personal_count(ITEM_REEDS, -(max_craft * 3)); add_personal_count(ITEM_REED_BASKETS, max_craft); - speak_with_history("Crafted " + max_craft + " Reed Baskets.", true); + dictionary reedBasketArgs; + reedBasketArgs.set("count", max_craft); + speak_with_history(trf("system.crafting.tools.crafted_max.reed_baskets", reedBasketArgs), true); } void craft_clay_pot() { string missing = ""; - if (get_personal_count(ITEM_CLAY) < 3) missing += "3 clay "; + if (get_personal_count(ITEM_CLAY) < 3) + missing += "3 clay "; // Check for fire within 3 tiles (can hear it) - WorldFire@ fire = get_fire_within_range(x, 3); + 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); add_personal_count(ITEM_CLAY, -3); add_personal_count(ITEM_CLAY_POTS, 1); - speak_with_history("Crafted a clay pot.", true); + speak_with_history(tr("system.crafting.tools.crafted.clay_pot"), true); } else { - speak_with_history("Missing: " + missing, true); + speak_crafting_missing(missing); } } void craft_clay_pot_max() { // Check for fire within 3 tiles (can hear it) - WorldFire@ fire = get_fire_within_range(x, 3); + 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; } int max_craft = get_personal_count(ITEM_CLAY) / 3; int space = get_personal_stack_limit() - get_personal_count(ITEM_CLAY_POTS); - if (max_craft > space) max_craft = space; + if (max_craft > space) + max_craft = space; if (max_craft <= 0) { - speak_with_history("Missing: 3 clay", true); + speak_crafting_missing("3 clay"); return; } @@ -474,5 +550,7 @@ void craft_clay_pot_max() { simulate_crafting(craft_time); add_personal_count(ITEM_CLAY, -(max_craft * 3)); add_personal_count(ITEM_CLAY_POTS, max_craft); - speak_with_history("Crafted " + max_craft + " Clay Pots.", true); + dictionary clayPotArgs; + clayPotArgs.set("count", max_craft); + speak_with_history(trf("system.crafting.tools.crafted_max.clay_pots", clayPotArgs), true); } diff --git a/src/crafting/craft_weapons.nvgt b/src/crafting/craft_weapons.nvgt index de36f6c..10d5aa7 100644 --- a/src/crafting/craft_weapons.nvgt +++ b/src/crafting/craft_weapons.nvgt @@ -1,50 +1,55 @@ // 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) { + while (true) { wait(5); if (menu_background_tick()) { return; } if (key_pressed(KEY_ESCAPE)) { - speak_with_history("Closed.", true); + speak_with_history(tr("system.menu.closed"), true); break; } 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); } if (key_pressed(KEY_RETURN)) { play_menu_select_sound(); - if (selection == 0) craft_spear(); - else if (selection == 1) craft_sling(); - else if (selection == 2) craft_bow(); + if (selection == 0) + craft_spear(); + else if (selection == 1) + craft_sling(); + else if (selection == 2) + craft_bow(); break; } if (key_pressed(KEY_TAB)) { play_menu_select_sound(); - if (selection == 0) craft_spear_max(); - else if (selection == 1) craft_sling_max(); - else if (selection == 2) craft_bow_max(); + if (selection == 0) + craft_spear_max(); + else if (selection == 1) + craft_sling_max(); + else if (selection == 2) + craft_bow_max(); break; } } @@ -52,14 +57,18 @@ void run_weapons_menu() { void craft_spear() { string missing = ""; - if (get_personal_count(ITEM_KNIVES) < 1) missing += "Stone Knife "; - if (get_personal_count(ITEM_STICKS) < 1) missing += "1 stick "; - if (get_personal_count(ITEM_VINES) < 1) missing += "1 vine "; - if (get_personal_count(ITEM_STONES) < 1) missing += "1 stone "; + if (get_personal_count(ITEM_KNIVES) < 1) + missing += "Stone Knife "; + if (get_personal_count(ITEM_STICKS) < 1) + missing += "1 stick "; + if (get_personal_count(ITEM_VINES) < 1) + missing += "1 vine "; + if (get_personal_count(ITEM_STONES) < 1) + missing += "1 stone "; 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); @@ -67,19 +76,19 @@ void craft_spear() { add_personal_count(ITEM_VINES, -1); add_personal_count(ITEM_STONES, -1); add_personal_count(ITEM_SPEARS, 1); - speak_with_history("Crafted a Spear.", true); + speak_with_history(tr("system.crafting.weapons.crafted.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; } @@ -87,18 +96,24 @@ void craft_spear_max() { int max_by_vines = get_personal_count(ITEM_VINES); int max_by_stones = get_personal_count(ITEM_STONES); int max_craft = max_by_sticks; - if (max_by_vines < max_craft) max_craft = max_by_vines; - if (max_by_stones < max_craft) max_craft = max_by_stones; + if (max_by_vines < max_craft) + max_craft = max_by_vines; + if (max_by_stones < max_craft) + max_craft = max_by_stones; int space = get_personal_stack_limit() - get_personal_count(ITEM_SPEARS); - if (max_craft > space) max_craft = space; + if (max_craft > space) + max_craft = space; if (max_craft <= 0) { string missing = ""; - if (get_personal_count(ITEM_STICKS) < 1) missing += "1 stick "; - if (get_personal_count(ITEM_VINES) < 1) missing += "1 vine "; - if (get_personal_count(ITEM_STONES) < 1) missing += "1 stone "; - speak_with_history("Missing: " + missing, true); + if (get_personal_count(ITEM_STICKS) < 1) + missing += "1 stick "; + if (get_personal_count(ITEM_VINES) < 1) + missing += "1 vine "; + if (get_personal_count(ITEM_STONES) < 1) + missing += "1 stone "; + speak_crafting_missing(missing); return; } @@ -109,48 +124,56 @@ void craft_spear_max() { add_personal_count(ITEM_VINES, -max_craft); add_personal_count(ITEM_STONES, -max_craft); add_personal_count(ITEM_SPEARS, max_craft); - speak_with_history("Crafted " + max_craft + " Spears.", true); + dictionary spearArgs; + spearArgs.set("count", max_craft); + speak_with_history(trf("system.crafting.weapons.crafted_max.spears", spearArgs), true); } void craft_sling() { string missing = ""; - if (get_personal_count(ITEM_SKINS) < 1) missing += "1 skin "; - if (get_personal_count(ITEM_VINES) < 2) missing += "2 vines "; + if (get_personal_count(ITEM_SKINS) < 1) + missing += "1 skin "; + if (get_personal_count(ITEM_VINES) < 2) + missing += "2 vines "; 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); add_personal_count(ITEM_SKINS, -1); add_personal_count(ITEM_VINES, -2); add_personal_count(ITEM_SLINGS, 1); - speak_with_history("Crafted a Sling.", true); + speak_with_history(tr("system.crafting.weapons.crafted.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; } int max_by_skins = get_personal_count(ITEM_SKINS); int max_by_vines = get_personal_count(ITEM_VINES) / 2; int max_craft = max_by_skins; - if (max_by_vines < max_craft) max_craft = max_by_vines; + if (max_by_vines < max_craft) + max_craft = max_by_vines; int space = get_personal_stack_limit() - get_personal_count(ITEM_SLINGS); - if (max_craft > space) max_craft = space; + if (max_craft > space) + max_craft = space; if (max_craft <= 0) { string missing = ""; - if (get_personal_count(ITEM_SKINS) < 1) missing += "1 skin "; - if (get_personal_count(ITEM_VINES) < 2) missing += "2 vines "; - speak_with_history("Missing: " + missing, true); + if (get_personal_count(ITEM_SKINS) < 1) + missing += "1 skin "; + if (get_personal_count(ITEM_VINES) < 2) + missing += "2 vines "; + speak_crafting_missing(missing); return; } @@ -160,48 +183,56 @@ void craft_sling_max() { add_personal_count(ITEM_SKINS, -max_craft); add_personal_count(ITEM_VINES, -(max_craft * 2)); add_personal_count(ITEM_SLINGS, max_craft); - speak_with_history("Crafted " + max_craft + " Slings.", true); + dictionary slingArgs; + slingArgs.set("count", max_craft); + speak_with_history(trf("system.crafting.weapons.crafted_max.slings", slingArgs), true); } void craft_bow() { string missing = ""; - if (get_personal_count(ITEM_STICKS) < 1) missing += "1 stick "; - if (get_personal_count(ITEM_BOWSTRINGS) < 1) missing += "1 bowstring "; + if (get_personal_count(ITEM_STICKS) < 1) + missing += "1 stick "; + if (get_personal_count(ITEM_BOWSTRINGS) < 1) + missing += "1 bowstring "; 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); add_personal_count(ITEM_STICKS, -1); add_personal_count(ITEM_BOWSTRINGS, -1); add_personal_count(ITEM_BOWS, 1); - speak_with_history("Crafted a Bow.", true); + speak_with_history(tr("system.crafting.weapons.crafted.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; } int max_by_sticks = get_personal_count(ITEM_STICKS); int max_by_bowstrings = get_personal_count(ITEM_BOWSTRINGS); int max_craft = max_by_sticks; - if (max_by_bowstrings < max_craft) max_craft = max_by_bowstrings; + if (max_by_bowstrings < max_craft) + max_craft = max_by_bowstrings; int space = get_personal_stack_limit() - get_personal_count(ITEM_BOWS); - if (max_craft > space) max_craft = space; + if (max_craft > space) + max_craft = space; if (max_craft <= 0) { string missing = ""; - if (get_personal_count(ITEM_STICKS) < 1) missing += "1 stick "; - if (get_personal_count(ITEM_BOWSTRINGS) < 1) missing += "1 bowstring "; - speak_with_history("Missing: " + missing, true); + if (get_personal_count(ITEM_STICKS) < 1) + missing += "1 stick "; + if (get_personal_count(ITEM_BOWSTRINGS) < 1) + missing += "1 bowstring "; + speak_crafting_missing(missing); return; } @@ -211,19 +242,25 @@ void craft_bow_max() { add_personal_count(ITEM_STICKS, -max_craft); add_personal_count(ITEM_BOWSTRINGS, -max_craft); add_personal_count(ITEM_BOWS, max_craft); - speak_with_history("Crafted " + max_craft + " Bows.", true); + dictionary bowArgs; + bowArgs.set("count", max_craft); + speak_with_history(trf("system.crafting.weapons.crafted_max.bows", bowArgs), true); } void craft_axe() { string missing = ""; - if (get_personal_count(ITEM_KNIVES) < 1) missing += "Stone Knife "; - if (get_personal_count(ITEM_STICKS) < 1) missing += "1 stick "; - if (get_personal_count(ITEM_VINES) < 1) missing += "1 vine "; - if (get_personal_count(ITEM_STONES) < 2) missing += "2 stones "; + if (get_personal_count(ITEM_KNIVES) < 1) + missing += "Stone Knife "; + if (get_personal_count(ITEM_STICKS) < 1) + missing += "1 stick "; + if (get_personal_count(ITEM_VINES) < 1) + missing += "1 vine "; + if (get_personal_count(ITEM_STONES) < 2) + missing += "2 stones "; 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); @@ -231,19 +268,19 @@ void craft_axe() { add_personal_count(ITEM_VINES, -1); add_personal_count(ITEM_STONES, -2); add_personal_count(ITEM_AXES, 1); - speak_with_history("Crafted a Stone Axe.", true); + speak_with_history(tr("system.crafting.weapons.crafted.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; } @@ -251,18 +288,24 @@ void craft_axe_max() { int max_by_vines = get_personal_count(ITEM_VINES); int max_by_stones = get_personal_count(ITEM_STONES) / 2; int max_craft = max_by_sticks; - if (max_by_vines < max_craft) max_craft = max_by_vines; - if (max_by_stones < max_craft) max_craft = max_by_stones; + if (max_by_vines < max_craft) + max_craft = max_by_vines; + if (max_by_stones < max_craft) + max_craft = max_by_stones; int space = get_personal_stack_limit() - get_personal_count(ITEM_AXES); - if (max_craft > space) max_craft = space; + if (max_craft > space) + max_craft = space; if (max_craft <= 0) { string missing = ""; - if (get_personal_count(ITEM_STICKS) < 1) missing += "1 stick "; - if (get_personal_count(ITEM_VINES) < 1) missing += "1 vine "; - if (get_personal_count(ITEM_STONES) < 2) missing += "2 stones "; - speak_with_history("Missing: " + missing, true); + if (get_personal_count(ITEM_STICKS) < 1) + missing += "1 stick "; + if (get_personal_count(ITEM_VINES) < 1) + missing += "1 vine "; + if (get_personal_count(ITEM_STONES) < 2) + missing += "2 stones "; + speak_crafting_missing(missing); return; } @@ -272,5 +315,7 @@ void craft_axe_max() { add_personal_count(ITEM_VINES, -max_craft); add_personal_count(ITEM_STONES, -(max_craft * 2)); add_personal_count(ITEM_AXES, max_craft); - speak_with_history("Crafted " + max_craft + " Stone Axes.", true); + dictionary axeArgs; + axeArgs.set("count", max_craft); + speak_with_history(trf("system.crafting.weapons.crafted_max.stone_axes", axeArgs), true); } diff --git a/src/crafting/crafting_core.nvgt b/src/crafting/crafting_core.nvgt index d0ffe4d..764ad4a 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,79 +132,88 @@ 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) { + while (true) { wait(5); if (menu_background_tick()) { return; } if (key_pressed(KEY_ESCAPE)) { - speak_with_history("Closed.", true); + speak_with_history(tr("system.menu.closed"), true); break; } if (key_pressed(KEY_DOWN)) { play_menu_move_sound(); selection++; - if (selection >= int(categories.length())) selection = 0; + if (selection >= int(categories.length())) + selection = 0; speak_with_history(categories[selection], true); } if (key_pressed(KEY_UP)) { play_menu_move_sound(); selection--; - if (selection < 0) selection = int(categories.length()) - 1; + if (selection < 0) + selection = int(categories.length()) - 1; speak_with_history(categories[selection], true); } if (key_pressed(KEY_RETURN)) { play_menu_select_sound(); int category = category_types[selection]; - if (category == 0) run_weapons_menu(); - else if (category == 1) run_tools_menu(); - else if (category == 2) run_materials_menu(); - else if (category == 3) run_clothing_menu(); - else if (category == 4) run_buildings_menu(); - else if (category == 5) run_barricade_menu(); - else if (category == 6) run_runes_menu(); + if (category == 0) + run_weapons_menu(); + else if (category == 1) + run_tools_menu(); + else if (category == 2) + run_materials_menu(); + else if (category == 3) + run_clothing_menu(); + else if (category == 4) + run_buildings_menu(); + else if (category == 5) + run_barricade_menu(); + else if (category == 6) + run_runes_menu(); break; } } } void simulate_crafting(int item_count) { - speak_with_history("Crafting...", true); + speak_with_history(tr("system.crafting.in_progress"), true); // Nothing should take less than 4. - if(item_count < 4) { + if (item_count < 4) { item_count = 4; } - for(int i = 0; i < item_count; i++) { + for (int i = 0; i < item_count; i++) { float pitch = random(85, 115); p.play_stationary_extended("sounds/crafting.ogg", false, 0, 0, 0, pitch); timer t; - while(t.elapsed < 800) { + while (t.elapsed < 800) { wait(5); if (menu_background_tick()) { return; diff --git a/src/creature_audio.nvgt b/src/creature_audio.nvgt index d650a80..0094dd0 100644 --- a/src/creature_audio.nvgt +++ b/src/creature_audio.nvgt @@ -46,15 +46,16 @@ const int CREATURE_DEFAULT_FOOTSTEP_DISTANCE = 6; // Plays a creature's voice/alert sound with consistent positioning // Returns the sound handle for tracking -int play_creature_voice(string sound_file, int listener_x, int creature_x, float volume_step = CREATURE_DEFAULT_VOLUME_STEP) -{ +int play_creature_voice(string sound_file, int listener_x, int creature_x, + float volume_step = CREATURE_DEFAULT_VOLUME_STEP) { return play_1d_with_volume_step(sound_file, listener_x, creature_x, false, volume_step); } // Plays a creature's footstep sound with consistent positioning // Only plays if within max_distance to avoid cluttering the soundscape -void play_creature_footstep(int listener_x, int creature_x, int base_end, int grass_end, int max_distance = CREATURE_DEFAULT_FOOTSTEP_DISTANCE, float volume_step = CREATURE_DEFAULT_VOLUME_STEP) -{ +void play_creature_footstep(int listener_x, int creature_x, int base_end, int grass_end, + int max_distance = CREATURE_DEFAULT_FOOTSTEP_DISTANCE, + float volume_step = CREATURE_DEFAULT_VOLUME_STEP) { int distance = creature_x - listener_x; if (distance < 0) { distance = -distance; @@ -68,19 +69,18 @@ void play_creature_footstep(int listener_x, int creature_x, int base_end, int gr } // Plays a creature attack sound (hitting player, hitting barricade, etc.) -void play_creature_attack_sound(string sound_file, int listener_x, int creature_x, float volume_step = CREATURE_DEFAULT_VOLUME_STEP) -{ +void play_creature_attack_sound(string sound_file, int listener_x, int creature_x, + float volume_step = CREATURE_DEFAULT_VOLUME_STEP) { play_1d_with_volume_step(sound_file, listener_x, creature_x, false, volume_step); } // Plays a creature death/fall sound -void play_creature_death_sound(string sound_file, int listener_x, int creature_x, float volume_step = CREATURE_DEFAULT_VOLUME_STEP) -{ +void play_creature_death_sound(string sound_file, int listener_x, int creature_x, + float volume_step = CREATURE_DEFAULT_VOLUME_STEP) { play_1d_with_volume_step(sound_file, listener_x, creature_x, false, volume_step); } -string get_creature_death_sound_from_alert(string alert_sound) -{ +string get_creature_death_sound_from_alert(string alert_sound) { if (alert_sound == "") { return ""; } @@ -98,7 +98,7 @@ string get_creature_death_sound_from_alert(string alert_sound) if (filename == "bandit3" || filename == "bandit4") { string female_death = "sounds/enemies/bandit_female_dies.ogg"; - if (file_exists(female_death)) { + if (audio_asset_exists(female_death)) { return female_death; } } @@ -118,15 +118,15 @@ string get_creature_death_sound_from_alert(string alert_sound) } string death_sound = "sounds/enemies/" + filename + "_dies.ogg"; - if (!file_exists(death_sound)) { + if (!audio_asset_exists(death_sound)) { return ""; } return death_sound; } -void play_creature_death_sounds(string default_sound, string alert_sound, int listener_x, int creature_x, float volume_step = CREATURE_DEFAULT_VOLUME_STEP) -{ +void play_creature_death_sounds(string default_sound, string alert_sound, int listener_x, int creature_x, + float volume_step = CREATURE_DEFAULT_VOLUME_STEP) { play_creature_death_sound(default_sound, listener_x, creature_x, volume_step); string death_sound = get_creature_death_sound_from_alert(alert_sound); @@ -136,7 +136,7 @@ void play_creature_death_sounds(string default_sound, string alert_sound, int li } // Plays a creature hit/damage sound (when player damages the creature) -void play_creature_hit_sound(string sound_file, int listener_x, int creature_x, float volume_step = CREATURE_DEFAULT_VOLUME_STEP) -{ +void play_creature_hit_sound(string sound_file, int listener_x, int creature_x, + float volume_step = CREATURE_DEFAULT_VOLUME_STEP) { play_1d_with_volume_step(sound_file, listener_x, creature_x, false, volume_step); } diff --git a/src/enemies/bandit.nvgt b/src/enemies/bandit.nvgt index 7401a15..be97f16 100644 --- a/src/enemies/bandit.nvgt +++ b/src/enemies/bandit.nvgt @@ -3,7 +3,7 @@ string[] bandit_sounds = {"sounds/enemies/bandit1.ogg", "sounds/enemies/bandit2.ogg"}; -string[] get_invader_sound_list(const string&in invader_type) { +string[] get_invader_sound_list(const string& in invader_type) { string[] sounds; if (invader_type == "") { return sounds; @@ -11,7 +11,7 @@ string[] get_invader_sound_list(const string&in invader_type) { for (int i = 1; i <= INVADER_SOUND_VARIANTS_MAX; i++) { string sound_file = "sounds/enemies/" + invader_type + i + ".ogg"; - if (file_exists(sound_file)) { + if (audio_asset_exists(sound_file)) { sounds.insert_last(sound_file); } } @@ -19,7 +19,7 @@ string[] get_invader_sound_list(const string&in invader_type) { return sounds; } -string pick_invader_alert_sound(const string&in invader_type) { +string pick_invader_alert_sound(const string& in invader_type) { string[] sounds = get_invader_sound_list(invader_type); if (sounds.length() == 0) { sounds = bandit_sounds; @@ -49,7 +49,7 @@ class Bandit { // Wandering behavior properties string behavior_state; // "aggressive" or "wandering" - int wander_direction; // -1, 0, or 1 + int wander_direction; // -1, 0, or 1 timer wander_direction_timer; int wander_direction_change_interval; @@ -89,12 +89,12 @@ class Bandit { // Initialize wandering behavior (start aggressive during invasion) behavior_state = "aggressive"; wander_direction = 0; - wander_direction_change_interval = random(BANDIT_WANDER_DIRECTION_CHANGE_MIN, BANDIT_WANDER_DIRECTION_CHANGE_MAX); + wander_direction_change_interval = + random(BANDIT_WANDER_DIRECTION_CHANGE_MIN, BANDIT_WANDER_DIRECTION_CHANGE_MAX); wander_direction_timer.restart(); in_weapon_range = false; } -} -Bandit@[] bandits; +} Bandit @[] bandits; void update_bandit_weapon_range_audio() { for (uint i = 0; i < bandits.length(); i++) { @@ -104,12 +104,14 @@ void update_bandit_weapon_range_audio() { bool bandit_range_audio_registered = false; void ensure_bandit_range_audio_registration() { - if (bandit_range_audio_registered) return; + if (bandit_range_audio_registered) + return; bandit_range_audio_registered = register_weapon_range_audio_callback(@update_bandit_weapon_range_audio); } void clear_bandits() { - if (bandits.length() == 0) return; + if (bandits.length() == 0) + return; for (uint i = 0; i < bandits.length(); i++) { force_weapon_range_exit(bandits[i].position, bandits[i].in_weapon_range); @@ -121,7 +123,7 @@ void clear_bandits() { bandits.resize(0); } -Bandit@ get_bandit_at(int pos) { +Bandit @get_bandit_at(int pos) { for (uint i = 0; i < bandits.length(); i++) { if (bandits[i].position == pos) { return @bandits[i]; @@ -141,14 +143,18 @@ int pick_bandit_spawn_position(int range_start, int range_end) { for (int attempts = 0; attempts < 20; attempts++) { int candidate = random(start, end); - if (candidate == x) continue; - if (get_bandit_at(candidate) != null) continue; + if (candidate == x) + continue; + if (get_bandit_at(candidate) != null) + continue; return candidate; } for (int candidate = start; candidate <= end; candidate++) { - if (candidate == x) continue; - if (get_bandit_at(candidate) != null) continue; + if (candidate == x) + continue; + if (get_bandit_at(candidate) != null) + continue; return candidate; } @@ -158,7 +164,8 @@ int pick_bandit_spawn_position(int range_start, int range_end) { int pick_bandit_spawn_east_of_player(int min_distance, int max_distance, int range_start, int range_end) { int min_dist = min_distance; int max_dist = max_distance; - if (min_dist < 0) min_dist = 0; + if (min_dist < 0) + min_dist = 0; if (max_dist < min_dist) { int temp = min_dist; min_dist = max_dist; @@ -176,10 +183,13 @@ int pick_bandit_spawn_east_of_player(int min_distance, int max_distance, int ran range_end_norm = temp; } - if (start < range_start_norm) start = range_start_norm; - if (end > range_end_norm) end = range_end_norm; + if (start < range_start_norm) + start = range_start_norm; + if (end > range_end_norm) + end = range_end_norm; - if (start > end) return -1; + if (start > end) + return -1; return pick_bandit_spawn_position(start, end); } @@ -187,7 +197,8 @@ int pick_bandit_spawn_east_of_player(int min_distance, int max_distance, int ran int pick_bandit_spawn_west_of_player(int min_distance, int max_distance, int range_start, int range_end) { int min_dist = min_distance; int max_dist = max_distance; - if (min_dist < 0) min_dist = 0; + if (min_dist < 0) + min_dist = 0; if (max_dist < min_dist) { int temp = min_dist; min_dist = max_dist; @@ -205,29 +216,38 @@ int pick_bandit_spawn_west_of_player(int min_distance, int max_distance, int ran range_end_norm = temp; } - if (start < range_start_norm) start = range_start_norm; - if (end > range_end_norm) end = range_end_norm; + if (start < range_start_norm) + start = range_start_norm; + if (end > range_end_norm) + end = range_end_norm; - if (start > end) return -1; + if (start > end) + return -1; return pick_bandit_spawn_position(start, end); } int pick_bandit_spawn_near_player(int range_start, int range_end) { // Preferred: 30-50 tiles east of player. - int spawn_x = pick_bandit_spawn_east_of_player(BANDIT_SPAWN_MIN_DISTANCE, BANDIT_SPAWN_MAX_DISTANCE, range_start, range_end); - if (spawn_x != -1) return spawn_x; + int spawn_x = + pick_bandit_spawn_east_of_player(BANDIT_SPAWN_MIN_DISTANCE, BANDIT_SPAWN_MAX_DISTANCE, range_start, range_end); + if (spawn_x != -1) + return spawn_x; // Fallback: 30-50 tiles west when east side is not available. - spawn_x = pick_bandit_spawn_west_of_player(BANDIT_SPAWN_MIN_DISTANCE, BANDIT_SPAWN_MAX_DISTANCE, range_start, range_end); - if (spawn_x != -1) return spawn_x; + spawn_x = + pick_bandit_spawn_west_of_player(BANDIT_SPAWN_MIN_DISTANCE, BANDIT_SPAWN_MAX_DISTANCE, range_start, range_end); + if (spawn_x != -1) + return spawn_x; // If map bounds are tight, relax minimum distance but keep around player. spawn_x = pick_bandit_spawn_east_of_player(1, BANDIT_SPAWN_MAX_DISTANCE, range_start, range_end); - if (spawn_x != -1) return spawn_x; + if (spawn_x != -1) + return spawn_x; spawn_x = pick_bandit_spawn_west_of_player(1, BANDIT_SPAWN_MAX_DISTANCE, range_start, range_end); - if (spawn_x != -1) return spawn_x; + if (spawn_x != -1) + return spawn_x; return -1; } @@ -250,7 +270,7 @@ int count_bandits_in_range(int range_start, int range_end) { return count; } -void spawn_bandit(int expansion_start, int expansion_end, const string&in invader_type = "bandit") { +void spawn_bandit(int expansion_start, int expansion_end, const string& in invader_type = "bandit") { int spawn_x = -1; if (invasion_active) { spawn_x = pick_bandit_spawn_near_player(expansion_start, expansion_end); @@ -258,7 +278,8 @@ void spawn_bandit(int expansion_start, int expansion_end, const string&in invade if (spawn_x == -1) { spawn_x = pick_bandit_spawn_position(expansion_start, expansion_end); } - if (spawn_x == -1) return; + if (spawn_x == -1) + return; int home_start = expansion_start; int home_end = expansion_end; @@ -271,7 +292,7 @@ void spawn_bandit(int expansion_start, int expansion_end, const string&in invade } } - Bandit@ b = Bandit(spawn_x, home_start, home_end, invader_type); + Bandit @b = Bandit(spawn_x, home_start, home_end, invader_type); if (!invasion_active) { b.behavior_state = "wandering"; } @@ -285,7 +306,7 @@ void spawn_bandit(int expansion_start, int expansion_end, const string&in invade } } -bool can_bandit_attack_player(Bandit@ bandit) { +bool can_bandit_attack_player(Bandit @bandit) { if (player_health <= 0) { return false; } @@ -302,7 +323,7 @@ bool can_bandit_attack_player(Bandit@ bandit) { return y <= BANDIT_ATTACK_MAX_HEIGHT; } -bool try_attack_player_bandit(Bandit@ bandit) { +bool try_attack_player_bandit(Bandit @bandit) { if (!can_bandit_attack_player(bandit)) { return false; } @@ -336,16 +357,19 @@ bool try_attack_player_bandit(Bandit@ bandit) { return true; } -void try_attack_barricade_bandit(Bandit@ bandit) { - if (barricade_health <= 0) return; - if (bandit.attack_timer.elapsed < BANDIT_ATTACK_INTERVAL) return; +void try_attack_barricade_bandit(Bandit @bandit) { + if (barricade_health <= 0) + return; + if (bandit.attack_timer.elapsed < BANDIT_ATTACK_INTERVAL) + return; bandit.attack_timer.restart(); // Bandits do 1-2 damage to barricade int damage = random(BANDIT_DAMAGE_MIN, BANDIT_DAMAGE_MAX); barricade_health -= damage; - if (barricade_health < 0) barricade_health = 0; + if (barricade_health < 0) + barricade_health = 0; // Play weapon swing sound (barricade hits share a common impact sound) if (bandit.weapon_type == "spear") { @@ -362,17 +386,19 @@ void try_attack_barricade_bandit(Bandit@ bandit) { int before_health = bandit.health; damage_bandit_at(bandit.position, counterDamage); if (before_health - counterDamage <= 0 && x <= BASE_END) { - speak_with_history("Residents killed an attacking bandit.", true); + dictionary residentsKilledArgs; + residentsKilledArgs.set("enemy", i18n_translate_fragment_value("bandit")); + speak_with_history(trf("system.enemies.residents_killed_attacker", residentsKilledArgs), true); } } } if (barricade_health == 0) { - notify("The barricade has fallen!"); + notify(tr("system.base.barricade_fallen")); } } -void update_bandit(Bandit@ bandit, bool audio_active) { +void update_bandit(Bandit @bandit, bool audio_active) { bool enforce_home = (!invasion_active && bandit.home_start <= bandit.home_end); if (enforce_home) { if (bandit.position < bandit.home_start) { @@ -395,14 +421,16 @@ void update_bandit(Bandit@ bandit, bool audio_active) { if (bandit.sound_handle != -1) { p.destroy_sound(bandit.sound_handle); } - bandit.sound_handle = play_1d_with_volume_step(bandit.alert_sound, x, bandit.position, true, BANDIT_SOUND_VOLUME_STEP); + bandit.sound_handle = + play_1d_with_volume_step(bandit.alert_sound, x, bandit.position, true, BANDIT_SOUND_VOLUME_STEP); } if (try_attack_player_bandit(bandit)) { return; } - if (bandit.move_timer.elapsed < bandit.move_interval) return; + if (bandit.move_timer.elapsed < bandit.move_interval) + return; bandit.move_timer.restart(); // If barricade is up and bandit is at the edge of base, attack barricade @@ -423,7 +451,8 @@ void update_bandit(Bandit@ bandit, bool audio_active) { if (bandit.wander_direction_timer.elapsed > bandit.wander_direction_change_interval) { // Time to change direction bandit.wander_direction = random(-1, 1); - bandit.wander_direction_change_interval = random(BANDIT_WANDER_DIRECTION_CHANGE_MIN, BANDIT_WANDER_DIRECTION_CHANGE_MAX); + bandit.wander_direction_change_interval = + random(BANDIT_WANDER_DIRECTION_CHANGE_MIN, BANDIT_WANDER_DIRECTION_CHANGE_MAX); bandit.wander_direction_timer.restart(); } @@ -444,7 +473,8 @@ void update_bandit(Bandit@ bandit, bool audio_active) { } else { bandit.position = target_x; if (audio_active) { - play_creature_footstep(x, bandit.position, BASE_END, GRASS_END, BANDIT_FOOTSTEP_MAX_DISTANCE, BANDIT_SOUND_VOLUME_STEP); + play_creature_footstep(x, bandit.position, BASE_END, GRASS_END, + BANDIT_FOOTSTEP_MAX_DISTANCE, BANDIT_SOUND_VOLUME_STEP); } } } else { @@ -479,7 +509,8 @@ void update_bandit(Bandit@ bandit, bool audio_active) { } int target_x = bandit.position + direction; - if (target_x < 0 || target_x >= MAP_SIZE) return; + if (target_x < 0 || target_x >= MAP_SIZE) + return; if (enforce_home && (target_x < bandit.home_start || target_x > bandit.home_end)) { return; @@ -493,7 +524,8 @@ void update_bandit(Bandit@ bandit, bool audio_active) { bandit.position = target_x; if (audio_active) { - play_creature_footstep(x, bandit.position, BASE_END, GRASS_END, BANDIT_FOOTSTEP_MAX_DISTANCE, BANDIT_SOUND_VOLUME_STEP); + play_creature_footstep(x, bandit.position, BASE_END, GRASS_END, BANDIT_FOOTSTEP_MAX_DISTANCE, + BANDIT_SOUND_VOLUME_STEP); } } } @@ -506,7 +538,8 @@ void update_bandits() { bool limit_audio = (areaStarts.length() > 0); for (uint i = 0; i < bandits.length(); i++) { - bool audio_active = !limit_audio || range_overlaps_active_areas(bandits[i].position, bandits[i].position, areaStarts, areaEnds); + bool audio_active = + !limit_audio || range_overlaps_active_areas(bandits[i].position, bandits[i].position, areaStarts, areaEnds); update_bandit(bandits[i], audio_active); } } @@ -520,7 +553,8 @@ bool damage_bandit_at(int pos, int damage) { p.destroy_sound(bandits[i].sound_handle); bandits[i].sound_handle = -1; } - play_creature_death_sounds("sounds/enemies/enemy_falls.ogg", bandits[i].alert_sound, x, pos, BANDIT_SOUND_VOLUME_STEP); + play_creature_death_sounds("sounds/enemies/enemy_falls.ogg", bandits[i].alert_sound, x, pos, + BANDIT_SOUND_VOLUME_STEP); bandits.remove_at(i); } return true; diff --git a/src/enemies/flying_creatures.nvgt b/src/enemies/flying_creatures.nvgt index d7d8478..df28b7c 100644 --- a/src/enemies/flying_creatures.nvgt +++ b/src/enemies/flying_creatures.nvgt @@ -26,8 +26,7 @@ class FlyingCreatureConfig { int max_count; int sight_range; bool flee_on_sight; -} -FlyingCreatureConfig@[] flying_creature_configs; +} FlyingCreatureConfig @[] flying_creature_configs; class FlyingCreature { int position; @@ -48,7 +47,7 @@ class FlyingCreature { timer fade_timer; bool in_weapon_range; - FlyingCreature(string type, int pos, int home_start, int home_end, FlyingCreatureConfig@ cfg) { + FlyingCreature(string type, int pos, int home_start, int home_end, FlyingCreatureConfig @cfg) { position = pos; health = cfg.health; height = random(cfg.min_height, cfg.max_height); @@ -70,8 +69,7 @@ class FlyingCreature { ready_to_remove = false; in_weapon_range = false; } -} -FlyingCreature@[] flying_creatures; +} FlyingCreature @[] flying_creatures; void update_flying_creature_weapon_range_audio() { for (uint i = 0; i < flying_creatures.length(); i++) { @@ -87,14 +85,16 @@ void update_flying_creature_weapon_range_audio() { bool flying_creature_range_audio_registered = false; void ensure_flying_creature_range_audio_registration() { - if (flying_creature_range_audio_registered) return; - flying_creature_range_audio_registered = register_weapon_range_audio_callback(@update_flying_creature_weapon_range_audio); + if (flying_creature_range_audio_registered) + return; + flying_creature_range_audio_registered = + register_weapon_range_audio_callback(@update_flying_creature_weapon_range_audio); } void init_flying_creature_configs() { flying_creature_configs.resize(0); - FlyingCreatureConfig@ goose_cfg = FlyingCreatureConfig(); + FlyingCreatureConfig @goose_cfg = FlyingCreatureConfig(); goose_cfg.id = "goose"; goose_cfg.drop_type = "goose"; goose_cfg.spawn_mode = "water"; @@ -118,7 +118,7 @@ void init_flying_creature_configs() { goose_cfg.flee_on_sight = false; flying_creature_configs.insert_last(goose_cfg); - FlyingCreatureConfig@ turkey_cfg = FlyingCreatureConfig(); + FlyingCreatureConfig @turkey_cfg = FlyingCreatureConfig(); turkey_cfg.id = "turkey"; turkey_cfg.drop_type = "turkey"; turkey_cfg.spawn_mode = "forest"; @@ -143,7 +143,7 @@ void init_flying_creature_configs() { flying_creature_configs.insert_last(turkey_cfg); } -FlyingCreatureConfig@ get_flying_creature_config(string creature_type) { +FlyingCreatureConfig @get_flying_creature_config(string creature_type) { for (uint i = 0; i < flying_creature_configs.length(); i++) { if (flying_creature_configs[i].id == creature_type) { return @flying_creature_configs[i]; @@ -152,7 +152,7 @@ FlyingCreatureConfig@ get_flying_creature_config(string creature_type) { return null; } -FlyingCreatureConfig@ get_flying_creature_config_by_drop_type(string drop_type) { +FlyingCreatureConfig @get_flying_creature_config_by_drop_type(string drop_type) { for (uint i = 0; i < flying_creature_configs.length(); i++) { if (flying_creature_configs[i].drop_type == drop_type) { return @flying_creature_configs[i]; @@ -176,7 +176,7 @@ void clear_flying_creatures() { flying_creatures.resize(0); } -FlyingCreature@ get_flying_creature_at(int pos) { +FlyingCreature @get_flying_creature_at(int pos) { for (uint i = 0; i < flying_creatures.length(); i++) { if (flying_creatures[i].position == pos) { return @flying_creatures[i]; @@ -195,9 +195,10 @@ int get_flying_creature_count(string creature_type) { return count; } -bool get_random_flying_creature_area(FlyingCreatureConfig@ cfg, int &out area_start, int &out area_end) { +bool get_random_flying_creature_area(FlyingCreatureConfig @cfg, int& out area_start, int& out area_end) { if (cfg.spawn_mode == "forest") { - if (!get_random_forest_area(area_start, area_end)) return false; + if (!get_random_forest_area(area_start, area_end)) + return false; } else { int stream_count = int(world_streams.length()); int mountain_stream_count = 0; @@ -206,7 +207,8 @@ bool get_random_flying_creature_area(FlyingCreatureConfig@ cfg, int &out area_st } int total_areas = stream_count + mountain_stream_count; - if (total_areas <= 0) return false; + if (total_areas <= 0) + return false; int pick = random(0, total_areas - 1); if (pick < stream_count) { @@ -229,15 +231,19 @@ bool get_random_flying_creature_area(FlyingCreatureConfig@ cfg, int &out area_st area_start -= cfg.max_dist_from_water; area_end += cfg.max_dist_from_water; - if (area_start < 0) area_start = 0; - if (area_end >= MAP_SIZE) area_end = MAP_SIZE - 1; + if (area_start < 0) + area_start = 0; + if (area_end >= MAP_SIZE) + area_end = MAP_SIZE - 1; return true; } -bool get_random_forest_area(int &out area_start, int &out area_end) { - if (expanded_area_start == -1) return false; +bool get_random_forest_area(int& out area_start, int& out area_end) { + if (expanded_area_start == -1) + return false; int total = int(expanded_terrain_types.length()); - if (total <= 0) return false; + if (total <= 0) + return false; int[] segment_starts; int[] segment_ends; @@ -257,7 +263,8 @@ bool get_random_forest_area(int &out area_start, int &out area_end) { if (nextTerrain.find("mountain:") == 0) { nextTerrain = nextTerrain.substr(9); } - if (nextTerrain != terrain) break; + if (nextTerrain != terrain) + break; index++; } int segment_end = index; @@ -268,7 +275,8 @@ bool get_random_forest_area(int &out area_start, int &out area_end) { index++; } - if (total_tiles <= 0) return false; + if (total_tiles <= 0) + return false; int pick = random(0, total_tiles - 1); for (uint i = 0; i < segment_starts.length(); i++) { @@ -284,8 +292,9 @@ bool get_random_forest_area(int &out area_start, int &out area_end) { return false; } -bool find_flying_creature_spawn(FlyingCreatureConfig@ cfg, int &out spawn_x, int &out area_start, int &out area_end) { - if (!get_random_flying_creature_area(cfg, area_start, area_end)) return false; +bool find_flying_creature_spawn(FlyingCreatureConfig @cfg, int& out spawn_x, int& out area_start, int& out area_end) { + if (!get_random_flying_creature_area(cfg, area_start, area_end)) + return false; for (int attempts = 0; attempts < 20; attempts++) { int candidate = random(area_start, area_end); @@ -297,7 +306,7 @@ bool find_flying_creature_spawn(FlyingCreatureConfig@ cfg, int &out spawn_x, int return false; } -void fly_away_flying_creature(FlyingCreature@ creature, FlyingCreatureConfig@ cfg) { +void fly_away_flying_creature(FlyingCreature @creature, FlyingCreatureConfig @cfg) { creature.state = "fading"; creature.fading_out = true; creature.ready_to_remove = false; @@ -314,8 +323,9 @@ void fly_away_flying_creature(FlyingCreature@ creature, FlyingCreatureConfig@ cf } bool spawn_flying_creature(string creature_type) { - FlyingCreatureConfig@ cfg = get_flying_creature_config(creature_type); - if (cfg is null) return false; + FlyingCreatureConfig @cfg = get_flying_creature_config(creature_type); + if (cfg is null) + return false; int spawn_x = -1; int area_start = 0; @@ -325,7 +335,7 @@ bool spawn_flying_creature(string creature_type) { return false; } - FlyingCreature@ c = FlyingCreature(creature_type, spawn_x, area_start, area_end, cfg); + FlyingCreature @c = FlyingCreature(creature_type, spawn_x, area_start, area_end, cfg); flying_creatures.insert_last(c); // Play looping sound that follows the flying creature int[] areaStarts; @@ -337,9 +347,10 @@ bool spawn_flying_creature(string creature_type) { return true; } -void update_flying_creature(FlyingCreature@ creature, bool audio_active) { - FlyingCreatureConfig@ cfg = get_flying_creature_config(creature.creature_type); - if (cfg is null) return; +void update_flying_creature(FlyingCreature @creature, bool audio_active) { + FlyingCreatureConfig @cfg = get_flying_creature_config(creature.creature_type); + if (cfg is null) + return; if (creature.state == "fading") { if (!creature.fading_out) { @@ -353,8 +364,10 @@ void update_flying_creature(FlyingCreature@ creature, bool audio_active) { creature.ready_to_remove = true; } else if (creature.sound_handle != -1 && p.sound_is_active(creature.sound_handle)) { float progress = float(creature.fade_timer.elapsed) / float(FLYING_CREATURE_FADE_OUT_DURATION); - if (progress < 0.0) progress = 0.0; - if (progress > 1.0) progress = 1.0; + if (progress < 0.0) + progress = 0.0; + if (progress > 1.0) + progress = 1.0; float volume = 0.0 + (FLYING_CREATURE_FADE_OUT_MIN_VOLUME * progress); p.update_sound_start_values(creature.sound_handle, 0.0, volume, 1.0); } @@ -388,7 +401,8 @@ void update_flying_creature(FlyingCreature@ creature, bool audio_active) { if (creature.sound_handle != -1) { p.destroy_sound(creature.sound_handle); } - creature.sound_handle = play_1d_with_volume_step(creature.voice_sound, x, creature.position, true, cfg.sound_volume_step); + creature.sound_handle = + play_1d_with_volume_step(creature.voice_sound, x, creature.position, true, cfg.sound_volume_step); } if (cfg.fly_away_chance > 0 && random(1, 1000) <= cfg.fly_away_chance) { @@ -404,11 +418,14 @@ void update_flying_creature(FlyingCreature@ creature, bool audio_active) { if (cfg.flee_on_sight && cfg.sight_range > 0) { int distance_to_player = abs(x - creature.position); if (distance_to_player <= cfg.sight_range) { - if (x > creature.position) dir = -1; - else if (x < creature.position) dir = 1; + if (x > creature.position) + dir = -1; + else if (x < creature.position) + dir = 1; } } - if (dir == 0) dir = random(-1, 1); + if (dir == 0) + dir = random(-1, 1); if (dir != 0) { int target_x = creature.position + dir; if (target_x < creature.area_start || target_x > creature.area_end) { @@ -434,11 +451,14 @@ void update_flying_creature(FlyingCreature@ creature, bool audio_active) { } float pitch_percent = 50.0 + (50.0 * (float(creature.height) / float(cfg.max_height))); - if (pitch_percent < 50.0) pitch_percent = 50.0; - if (pitch_percent > 100.0) pitch_percent = 100.0; + if (pitch_percent < 50.0) + pitch_percent = 50.0; + if (pitch_percent > 100.0) + pitch_percent = 100.0; if (audio_active) { - creature.fall_sound_handle = p.play_extended_1d(cfg.fall_sound, x, creature.position, 0, 0, true, 0, 0.0, 0.0, pitch_percent); + creature.fall_sound_handle = + p.play_extended_1d(cfg.fall_sound, x, creature.position, 0, 0, true, 0, 0.0, 0.0, pitch_percent); if (creature.fall_sound_handle != -1) { p.update_sound_positioning_values(creature.fall_sound_handle, -1.0, cfg.sound_volume_step, true); } @@ -466,7 +486,9 @@ void update_flying_creatures() { bool limit_audio = (areaStarts.length() > 0); for (uint i = 0; i < flying_creatures.length(); i++) { - bool audio_active = !limit_audio || range_overlaps_active_areas(flying_creatures[i].position, flying_creatures[i].position, areaStarts, areaEnds); + bool audio_active = + !limit_audio || range_overlaps_active_areas(flying_creatures[i].position, flying_creatures[i].position, + areaStarts, areaEnds); update_flying_creature(flying_creatures[i], audio_active); if (flying_creatures[i].health <= 0) { @@ -486,8 +508,9 @@ void update_flying_creatures() { void attempt_hourly_flying_creature_spawn() { for (uint i = 0; i < flying_creature_configs.length(); i++) { - FlyingCreatureConfig@ cfg = flying_creature_configs[i]; - if (get_flying_creature_count(cfg.id) >= cfg.max_count) continue; + FlyingCreatureConfig @cfg = flying_creature_configs[i]; + if (get_flying_creature_count(cfg.id) >= cfg.max_count) + continue; if (random(1, 100) <= cfg.hourly_spawn_chance) { spawn_flying_creature(cfg.id); } @@ -497,8 +520,9 @@ void attempt_hourly_flying_creature_spawn() { bool damage_flying_creature_at(int pos, int damage) { for (uint i = 0; i < flying_creatures.length(); i++) { if (flying_creatures[i].position == pos && flying_creatures[i].state == "flying") { - FlyingCreatureConfig@ cfg = get_flying_creature_config(flying_creatures[i].creature_type); - if (cfg is null) return false; + FlyingCreatureConfig @cfg = get_flying_creature_config(flying_creatures[i].creature_type); + if (cfg is null) + return false; flying_creatures[i].health -= damage; if (flying_creatures[i].health <= 0) { @@ -511,9 +535,11 @@ bool damage_flying_creature_at(int pos, int damage) { } float pitch_percent = 50.0 + (50.0 * (float(flying_creatures[i].height) / float(cfg.max_height))); - flying_creatures[i].fall_sound_handle = p.play_extended_1d(cfg.fall_sound, x, pos, 0, 0, true, 0, 0.0, 0.0, pitch_percent); + flying_creatures[i].fall_sound_handle = + p.play_extended_1d(cfg.fall_sound, x, pos, 0, 0, true, 0, 0.0, 0.0, pitch_percent); if (flying_creatures[i].fall_sound_handle != -1) { - p.update_sound_positioning_values(flying_creatures[i].fall_sound_handle, -1.0, cfg.sound_volume_step, true); + p.update_sound_positioning_values(flying_creatures[i].fall_sound_handle, -1.0, + cfg.sound_volume_step, true); } } return true; diff --git a/src/enemies/ground_game.nvgt b/src/enemies/ground_game.nvgt index 10c7845..459bcd9 100644 --- a/src/enemies/ground_game.nvgt +++ b/src/enemies/ground_game.nvgt @@ -15,7 +15,7 @@ class GroundGame { int area_start; int area_end; int wander_direction; // -1, 0, 1 - string animal_type; // "boar", future: "mountain_goat", "ram", etc. + string animal_type; // "boar", future: "mountain_goat", "ram", etc. bool in_weapon_range; GroundGame(int pos, int start, int end, string type = "boar") { @@ -36,8 +36,7 @@ class GroundGame { next_move_delay = random(BOAR_MOVE_INTERVAL_MIN, BOAR_MOVE_INTERVAL_MAX); in_weapon_range = false; } -} -GroundGame@[] ground_games; +} GroundGame @[] ground_games; void update_ground_game_weapon_range_audio() { for (uint i = 0; i < ground_games.length(); i++) { @@ -47,12 +46,14 @@ void update_ground_game_weapon_range_audio() { bool ground_game_range_audio_registered = false; void ensure_ground_game_range_audio_registration() { - if (ground_game_range_audio_registered) return; + if (ground_game_range_audio_registered) + return; ground_game_range_audio_registered = register_weapon_range_audio_callback(@update_ground_game_weapon_range_audio); } void clear_ground_games() { - if (ground_games.length() == 0) return; + if (ground_games.length() == 0) + return; for (uint i = 0; i < ground_games.length(); i++) { force_weapon_range_exit(ground_games[i].position, ground_games[i].in_weapon_range); @@ -64,7 +65,7 @@ void clear_ground_games() { ground_games.resize(0); } -GroundGame@ get_ground_game_at(int pos) { +GroundGame @get_ground_game_at(int pos) { for (uint i = 0; i < ground_games.length(); i++) { if (ground_games[i].position == pos) { return @ground_games[i]; @@ -80,16 +81,18 @@ void spawn_ground_game(int expansion_start, int expansion_end) { int candidate = random(expansion_start, expansion_end); // Don't spawn too close to base (keep away from BASE_END) - if (candidate <= BASE_END + 5) continue; + if (candidate <= BASE_END + 5) + continue; if (get_ground_game_at(candidate) == null) { spawn_x = candidate; break; } } - if (spawn_x == -1) return; // Failed to find spot + if (spawn_x == -1) + return; // Failed to find spot - GroundGame@ b = GroundGame(spawn_x, expansion_start, expansion_end, "boar"); + GroundGame @b = GroundGame(spawn_x, expansion_start, expansion_end, "boar"); ground_games.insert_last(b); // Play looping sound that follows the boar int[] areaStarts; @@ -100,34 +103,40 @@ void spawn_ground_game(int expansion_start, int expansion_end) { } } -bool can_ground_game_attack_player(GroundGame@ game) { - if (player_health <= 0) return false; +bool can_ground_game_attack_player(GroundGame @game) { + if (player_health <= 0) + return false; // Check if player is on ground (ground game can't fly/climb) - if (y > 0) return false; + if (y > 0) + return false; - if (abs(game.position - x) > 1) return false; + if (abs(game.position - x) > 1) + return false; return true; } -bool try_attack_player_ground_game(GroundGame@ game) { - if (!can_ground_game_attack_player(game)) return false; +bool try_attack_player_ground_game(GroundGame @game) { + if (!can_ground_game_attack_player(game)) + return false; - if (game.attack_timer.elapsed < BOAR_ATTACK_INTERVAL) return false; + if (game.attack_timer.elapsed < BOAR_ATTACK_INTERVAL) + return false; game.attack_timer.restart(); // Attack! int damage = random(BOAR_DAMAGE_MIN, BOAR_DAMAGE_MAX); player_health -= damage; - if (player_health < 0) player_health = 0; + if (player_health < 0) + player_health = 0; play_player_damage_sound(); return true; } -void update_ground_game(GroundGame@ game, bool audio_active) { +void update_ground_game(GroundGame @game, bool audio_active) { // Update looping sound position if (!audio_active) { if (game.sound_handle != -1) { @@ -152,7 +161,8 @@ void update_ground_game(GroundGame@ game, bool audio_active) { // Movement logic int move_speed = (game.state == "charging") ? BOAR_CHARGE_SPEED : game.next_move_delay; - if (game.move_timer.elapsed < move_speed) return; + if (game.move_timer.elapsed < move_speed) + return; game.move_timer.restart(); if (game.state == "wandering") { game.next_move_delay = random(BOAR_MOVE_INTERVAL_MIN, BOAR_MOVE_INTERVAL_MAX); @@ -175,10 +185,11 @@ void update_ground_game(GroundGame@ game, bool audio_active) { // Don't leave area or enter base if (target >= game.area_start && target <= game.area_end && target > BASE_END) { - game.position = target; - if (audio_active) { - play_creature_footstep(x, game.position, BASE_END, GRASS_END, BOAR_FOOTSTEP_MAX_DISTANCE, BOAR_SOUND_VOLUME_STEP); - } + game.position = target; + if (audio_active) { + play_creature_footstep(x, game.position, BASE_END, GRASS_END, BOAR_FOOTSTEP_MAX_DISTANCE, + BOAR_SOUND_VOLUME_STEP); + } } } else { // Wandering @@ -188,12 +199,13 @@ void update_ground_game(GroundGame@ game, bool audio_active) { if (game.wander_direction != 0) { int target = game.position + game.wander_direction; - // Don't leave area or enter base + // Don't leave area or enter base if (target >= game.area_start && target <= game.area_end && target > BASE_END) { - game.position = target; - if (audio_active) { - play_creature_footstep(x, game.position, BASE_END, GRASS_END, BOAR_FOOTSTEP_MAX_DISTANCE, BOAR_SOUND_VOLUME_STEP); - } + game.position = target; + if (audio_active) { + play_creature_footstep(x, game.position, BASE_END, GRASS_END, BOAR_FOOTSTEP_MAX_DISTANCE, + BOAR_SOUND_VOLUME_STEP); + } } else { game.wander_direction = -game.wander_direction; // Turn around } @@ -209,14 +221,17 @@ void update_ground_games() { bool limit_audio = (areaStarts.length() > 0); for (uint i = 0; i < ground_games.length(); i++) { - bool audio_active = !limit_audio || range_overlaps_active_areas(ground_games[i].position, ground_games[i].position, areaStarts, areaEnds); + bool audio_active = !limit_audio || range_overlaps_active_areas(ground_games[i].position, + ground_games[i].position, areaStarts, areaEnds); update_ground_game(ground_games[i], audio_active); } } void attempt_hourly_ground_game_spawn() { - if (expanded_area_start == -1) return; - if (ground_games.length() >= BOAR_MAX_COUNT) return; + if (expanded_area_start == -1) + return; + if (ground_games.length() >= BOAR_MAX_COUNT) + return; if (random(1, 100) <= BOAR_SPAWN_CHANCE_PER_HOUR) { spawn_ground_game(expanded_area_start, expanded_area_end); @@ -246,10 +261,22 @@ bool damage_ground_game_at(int pos, int damage) { } // Backward compatibility aliases (to be removed after refactoring) -GroundGame@[]@ boars = @ground_games; -GroundGame@ get_boar_at(int pos) { return get_ground_game_at(pos); } -void clear_boars() { clear_ground_games(); } -void spawn_boar(int expansion_start, int expansion_end) { spawn_ground_game(expansion_start, expansion_end); } -void update_boars() { update_ground_games(); } -void attempt_hourly_boar_spawn() { attempt_hourly_ground_game_spawn(); } -bool damage_boar_at(int pos, int damage) { return damage_ground_game_at(pos, damage); } +GroundGame @[] @boars = @ground_games; +GroundGame @get_boar_at(int pos) { + return get_ground_game_at(pos); +} +void clear_boars() { + clear_ground_games(); +} +void spawn_boar(int expansion_start, int expansion_end) { + spawn_ground_game(expansion_start, expansion_end); +} +void update_boars() { + update_ground_games(); +} +void attempt_hourly_boar_spawn() { + attempt_hourly_ground_game_spawn(); +} +bool damage_boar_at(int pos, int damage) { + return damage_ground_game_at(pos, damage); +} diff --git a/src/enemies/undead.nvgt b/src/enemies/undead.nvgt index fdc1669..dbac127 100644 --- a/src/enemies/undead.nvgt +++ b/src/enemies/undead.nvgt @@ -3,12 +3,8 @@ string[] undead_zombie_sounds = {"sounds/enemies/zombie1.ogg"}; string[] undead_wight_sounds = {"sounds/enemies/wight1.ogg"}; -string[] undead_vampyr_sounds = { - "sounds/enemies/vampyr1.ogg", - "sounds/enemies/vampyr2.ogg", - "sounds/enemies/vampyr3.ogg", - "sounds/enemies/vampyr4.ogg" -}; +string[] undead_vampyr_sounds = {"sounds/enemies/vampyr1.ogg", "sounds/enemies/vampyr2.ogg", + "sounds/enemies/vampyr3.ogg", "sounds/enemies/vampyr4.ogg"}; string[] undead_resident_sounds = {"sounds/enemies/undead_resident1.ogg"}; int wight_spawn_chance = WIGHT_SPAWN_CHANCE_START; @@ -16,28 +12,37 @@ int wight_spawned_this_night_count = 0; int vampyr_spawn_chance = VAMPYR_SPAWN_CHANCE_START; int vampyr_spawned_this_night_count = 0; -int get_undead_base_health(const string &in undead_type) { - if (undead_type == "wight") return WIGHT_HEALTH; - if (undead_type == "vampyr") return VAMPYR_HEALTH; - if (undead_type == "undead_resident") return UNDEAD_RESIDENT_HEALTH; +int get_undead_base_health(const string& in undead_type) { + if (undead_type == "wight") + return WIGHT_HEALTH; + if (undead_type == "vampyr") + return VAMPYR_HEALTH; + if (undead_type == "undead_resident") + return UNDEAD_RESIDENT_HEALTH; return ZOMBIE_HEALTH; } -int get_undead_damage_min(const string &in undead_type) { - if (undead_type == "wight") return WIGHT_DAMAGE_MIN; - if (undead_type == "vampyr") return WIGHT_DAMAGE_MIN; - if (undead_type == "undead_resident") return UNDEAD_RESIDENT_DAMAGE_MIN; +int get_undead_damage_min(const string& in undead_type) { + if (undead_type == "wight") + return WIGHT_DAMAGE_MIN; + if (undead_type == "vampyr") + return WIGHT_DAMAGE_MIN; + if (undead_type == "undead_resident") + return UNDEAD_RESIDENT_DAMAGE_MIN; return ZOMBIE_DAMAGE_MIN; } -int get_undead_damage_max(const string &in undead_type) { - if (undead_type == "wight") return WIGHT_DAMAGE_MAX; - if (undead_type == "vampyr") return WIGHT_DAMAGE_MAX; - if (undead_type == "undead_resident") return UNDEAD_RESIDENT_DAMAGE_MAX; +int get_undead_damage_max(const string& in undead_type) { + if (undead_type == "wight") + return WIGHT_DAMAGE_MAX; + if (undead_type == "vampyr") + return WIGHT_DAMAGE_MAX; + if (undead_type == "undead_resident") + return UNDEAD_RESIDENT_DAMAGE_MAX; return ZOMBIE_DAMAGE_MAX; } -string pick_undead_voice_sound(const string &in undead_type) { +string pick_undead_voice_sound(const string& in undead_type) { if (undead_type == "wight") { int sound_index = random(0, undead_wight_sounds.length() - 1); return undead_wight_sounds[sound_index]; @@ -54,10 +59,13 @@ string pick_undead_voice_sound(const string &in undead_type) { return undead_zombie_sounds[sound_index]; } -string get_undead_label(const string &in undead_type) { - if (undead_type == "wight") return "wight"; - if (undead_type == "vampyr") return "vampyr"; - if (undead_type == "undead_resident") return "undead resident"; +string get_undead_label(const string& in undead_type) { + if (undead_type == "wight") + return "wight"; + if (undead_type == "vampyr") + return "vampyr"; + if (undead_type == "undead_resident") + return "undead resident"; return "zombie"; } @@ -87,8 +95,7 @@ class Undead { move_timer.restart(); attack_timer.restart(); } -} -Undead@[] undeads; +} Undead @[] undeads; int count_wights() { int count = 0; @@ -119,7 +126,8 @@ bool has_vampyr() { } int get_night_special_undead_spawn_limit(int day) { - if (day < 1) day = 1; + if (day < 1) + day = 1; return 1 + (day / SPECIAL_UNDEAD_SPAWN_DAYS_PER_EXTRA); } @@ -131,12 +139,14 @@ void update_undead_weapon_range_audio() { bool undead_range_audio_registered = false; void ensure_undead_range_audio_registration() { - if (undead_range_audio_registered) return; + if (undead_range_audio_registered) + return; undead_range_audio_registered = register_weapon_range_audio_callback(@update_undead_weapon_range_audio); } void clear_undeads() { - if (undeads.length() == 0) return; + if (undeads.length() == 0) + return; for (uint i = 0; i < undeads.length(); i++) { force_weapon_range_exit(undeads[i].position, undeads[i].in_weapon_range); @@ -148,7 +158,7 @@ void clear_undeads() { undeads.resize(0); } -Undead@ get_undead_at(int pos) { +Undead @get_undead_at(int pos) { for (uint i = 0; i < undeads.length(); i++) { if (undeads[i].position == pos) { return @undeads[i]; @@ -168,14 +178,18 @@ int pick_undead_spawn_position(int range_start, int range_end) { for (int attempts = 0; attempts < 20; attempts++) { int candidate = random(start, end); - if (candidate == x) continue; - if (get_undead_at(candidate) != null) continue; + if (candidate == x) + continue; + if (get_undead_at(candidate) != null) + continue; return candidate; } for (int candidate = start; candidate <= end; candidate++) { - if (candidate == x) continue; - if (get_undead_at(candidate) != null) continue; + if (candidate == x) + continue; + if (get_undead_at(candidate) != null) + continue; return candidate; } @@ -185,7 +199,8 @@ int pick_undead_spawn_position(int range_start, int range_end) { int pick_undead_spawn_near_player(int min_distance, int max_distance) { int min_dist = min_distance; int max_dist = max_distance; - if (min_dist < 0) min_dist = 0; + if (min_dist < 0) + min_dist = 0; if (max_dist < min_dist) { int temp = min_dist; min_dist = max_dist; @@ -196,9 +211,12 @@ int pick_undead_spawn_near_player(int min_distance, int max_distance) { int distance = random(min_dist, max_dist); int direction = (random(0, 1) == 0) ? -1 : 1; int candidate = x + (distance * direction); - if (candidate <= BASE_END || candidate < 0 || candidate >= MAP_SIZE) continue; - if (candidate == x) continue; - if (get_undead_at(candidate) != null) continue; + if (candidate <= BASE_END || candidate < 0 || candidate >= MAP_SIZE) + continue; + if (candidate == x) + continue; + if (get_undead_at(candidate) != null) + continue; return candidate; } @@ -207,22 +225,30 @@ int pick_undead_spawn_near_player(int min_distance, int max_distance) { int right_start = x + min_dist; int right_end = x + max_dist; - if (left_start < BASE_END + 1) left_start = BASE_END + 1; - if (left_end < BASE_END + 1) left_end = BASE_END; - if (right_start < BASE_END + 1) right_start = BASE_END + 1; + if (left_start < BASE_END + 1) + left_start = BASE_END + 1; + if (left_end < BASE_END + 1) + left_end = BASE_END; + if (right_start < BASE_END + 1) + right_start = BASE_END + 1; - if (left_end >= MAP_SIZE) left_end = MAP_SIZE - 1; - if (right_start >= MAP_SIZE) right_start = MAP_SIZE; - if (right_end >= MAP_SIZE) right_end = MAP_SIZE - 1; + if (left_end >= MAP_SIZE) + left_end = MAP_SIZE - 1; + if (right_start >= MAP_SIZE) + right_start = MAP_SIZE; + if (right_end >= MAP_SIZE) + right_end = MAP_SIZE - 1; int candidate = -1; if (random(0, 1) == 0) { - if (left_start <= left_end) candidate = pick_undead_spawn_position(left_start, left_end); + if (left_start <= left_end) + candidate = pick_undead_spawn_position(left_start, left_end); if (candidate == -1 && right_start <= right_end) { candidate = pick_undead_spawn_position(right_start, right_end); } } else { - if (right_start <= right_end) candidate = pick_undead_spawn_position(right_start, right_end); + if (right_start <= right_end) + candidate = pick_undead_spawn_position(right_start, right_end); if (candidate == -1 && left_start <= left_end) { candidate = pick_undead_spawn_position(left_start, left_end); } @@ -231,7 +257,7 @@ int pick_undead_spawn_near_player(int min_distance, int max_distance) { return candidate; } -void spawn_undead(const string &in undead_type = "zombie") { +void spawn_undead(const string& in undead_type = "zombie") { int spawn_x = -1; if (undead_type == "zombie" || undead_type == "undead_resident") { spawn_x = pick_undead_spawn_near_player(ZOMBIE_SPAWN_MIN_DISTANCE, ZOMBIE_SPAWN_MAX_DISTANCE); @@ -239,9 +265,10 @@ void spawn_undead(const string &in undead_type = "zombie") { if (spawn_x == -1) { spawn_x = pick_undead_spawn_position(BASE_END + 1, MAP_SIZE - 1); } - if (spawn_x == -1) return; + if (spawn_x == -1) + return; - Undead@ undead = Undead(spawn_x, undead_type); + Undead @undead = Undead(spawn_x, undead_type); undeads.insert_last(undead); // Play looping sound that follows the undead int[] areaStarts; @@ -252,18 +279,22 @@ void spawn_undead(const string &in undead_type = "zombie") { if (undead_type == "vampyr") { undead.voice_sound = pick_undead_voice_sound(undead_type); } - undead.sound_handle = play_1d_with_volume_step(undead.voice_sound, x, spawn_x, loop_voice, ZOMBIE_SOUND_VOLUME_STEP); + undead.sound_handle = + play_1d_with_volume_step(undead.voice_sound, x, spawn_x, loop_voice, ZOMBIE_SOUND_VOLUME_STEP); } } -void try_attack_barricade_undead(Undead@ undead) { - if (barricade_health <= 0) return; - if (undead.attack_timer.elapsed < ZOMBIE_ATTACK_INTERVAL) return; +void try_attack_barricade_undead(Undead @undead) { + if (barricade_health <= 0) + return; + if (undead.attack_timer.elapsed < ZOMBIE_ATTACK_INTERVAL) + return; undead.attack_timer.restart(); int damage = random(get_undead_damage_min(undead.undead_type), get_undead_damage_max(undead.undead_type)); barricade_health -= damage; - if (barricade_health < 0) barricade_health = 0; + if (barricade_health < 0) + barricade_health = 0; play_creature_attack_sound("sounds/weapons/axe_hit.ogg", x, undead.position, ZOMBIE_SOUND_VOLUME_STEP); @@ -274,17 +305,19 @@ void try_attack_barricade_undead(Undead@ undead) { int before_health = undead.health; damage_undead_at(undead.position, counterDamage); if (before_health - counterDamage <= 0 && x <= BASE_END) { - speak_with_history("Residents killed an attacking " + get_undead_label(undead.undead_type) + ".", true); + dictionary residentsKilledArgs; + residentsKilledArgs.set("enemy", i18n_translate_fragment_value(get_undead_label(undead.undead_type))); + speak_with_history(trf("system.enemies.residents_killed_attacker", residentsKilledArgs), true); } } } if (barricade_health == 0) { - notify("The barricade has fallen!"); + notify(tr("system.base.barricade_fallen")); } } -bool can_undead_attack_player(Undead@ undead) { +bool can_undead_attack_player(Undead @undead) { if (player_health <= 0) { return false; } @@ -308,7 +341,7 @@ bool can_undead_attack_player(Undead@ undead) { return y <= ZOMBIE_ATTACK_MAX_HEIGHT; } -bool try_attack_player_undead(Undead@ undead) { +bool try_attack_player_undead(Undead @undead) { if (!can_undead_attack_player(undead)) { return false; } @@ -327,7 +360,7 @@ bool try_attack_player_undead(Undead@ undead) { return true; } -void start_vampyr_retreat(Undead@ undead) { +void start_vampyr_retreat(Undead @undead) { undead.retreating = true; undead.suppress_voice = true; if (undead.sound_handle != -1) { @@ -337,7 +370,7 @@ void start_vampyr_retreat(Undead@ undead) { undead.move_timer.restart(); } -void try_capture_resident_vampyr(Undead@ undead) { +void try_capture_resident_vampyr(Undead @undead) { if (undead.attack_timer.elapsed < VAMPYR_CAPTURE_INTERVAL) { return; } @@ -350,14 +383,15 @@ void try_capture_resident_vampyr(Undead@ undead) { if (random(1, 100) <= VAMPYR_CAPTURE_CHANCE) { residents_count--; - if (residents_count < 0) residents_count = 0; + if (residents_count < 0) + residents_count = 0; undead_residents_pending++; - speak_with_history("A resident has been taken.", true); + speak_with_history(tr("system.enemies.resident_taken"), true); start_vampyr_retreat(undead); } } -void update_undead(Undead@ undead, bool audio_active) { +void update_undead(Undead @undead, bool audio_active) { bool is_vampyr = (undead.undead_type == "vampyr"); bool loop_voice = !is_vampyr; @@ -377,19 +411,22 @@ void update_undead(Undead@ undead, bool audio_active) { if (is_vampyr) { undead.voice_sound = pick_undead_voice_sound(undead.undead_type); } - undead.sound_handle = play_1d_with_volume_step(undead.voice_sound, x, undead.position, loop_voice, ZOMBIE_SOUND_VOLUME_STEP); + undead.sound_handle = + play_1d_with_volume_step(undead.voice_sound, x, undead.position, loop_voice, ZOMBIE_SOUND_VOLUME_STEP); } if (try_attack_player_undead(undead)) { return; } - if (undead.undead_type == "vampyr" && !undead.retreating && barricade_health > 0 && undead.position == BASE_END + 1) { + if (undead.undead_type == "vampyr" && !undead.retreating && barricade_health > 0 && + undead.position == BASE_END + 1) { try_capture_resident_vampyr(undead); return; } - if (undead.move_timer.elapsed < ZOMBIE_MOVE_INTERVAL) return; + if (undead.move_timer.elapsed < ZOMBIE_MOVE_INTERVAL) + return; undead.move_timer.restart(); if (undead.undead_type != "vampyr" && barricade_health > 0 && undead.position == BASE_END + 1) { @@ -433,12 +470,14 @@ void update_undead(Undead@ undead, bool audio_active) { } } else { direction = random(-1, 1); - if (direction == 0) return; + if (direction == 0) + return; } } int target_x = undead.position + direction; - if (target_x < 0 || target_x >= MAP_SIZE) return; + if (target_x < 0 || target_x >= MAP_SIZE) + return; if (undead.undead_type != "vampyr" && target_x <= BASE_END && barricade_health > 0) { try_attack_barricade_undead(undead); @@ -447,7 +486,8 @@ void update_undead(Undead@ undead, bool audio_active) { undead.position = target_x; if (audio_active) { - play_creature_footstep(x, undead.position, BASE_END, GRASS_END, ZOMBIE_FOOTSTEP_MAX_DISTANCE, ZOMBIE_SOUND_VOLUME_STEP); + play_creature_footstep(x, undead.position, BASE_END, GRASS_END, ZOMBIE_FOOTSTEP_MAX_DISTANCE, + ZOMBIE_SOUND_VOLUME_STEP); } } @@ -478,7 +518,8 @@ void update_undeads() { if (zombie_swarm_active) { maxCount += ZOMBIE_SWARM_ZOMBIE_MAX_BONUS; } - if (maxCount > ZOMBIE_MAX_COUNT_CAP) maxCount = ZOMBIE_MAX_COUNT_CAP; + if (maxCount > ZOMBIE_MAX_COUNT_CAP) + maxCount = ZOMBIE_MAX_COUNT_CAP; int zombie_count = 0; int undead_resident_count = 0; @@ -508,7 +549,8 @@ void update_undeads() { bool limit_audio = (areaStarts.length() > 0); for (uint i = 0; i < undeads.length(); i++) { - bool audio_active = !limit_audio || range_overlaps_active_areas(undeads[i].position, undeads[i].position, areaStarts, areaEnds); + bool audio_active = + !limit_audio || range_overlaps_active_areas(undeads[i].position, undeads[i].position, areaStarts, areaEnds); update_undead(undeads[i], audio_active); } @@ -555,7 +597,8 @@ void attempt_hourly_wight_spawn() { } wight_spawn_chance += WIGHT_SPAWN_CHANCE_STEP; - if (wight_spawn_chance > 100) wight_spawn_chance = 100; + if (wight_spawn_chance > 100) + wight_spawn_chance = 100; } void attempt_hourly_vampyr_spawn() { @@ -594,7 +637,8 @@ void attempt_hourly_vampyr_spawn() { } vampyr_spawn_chance += VAMPYR_SPAWN_CHANCE_STEP; - if (vampyr_spawn_chance > 100) vampyr_spawn_chance = 100; + if (vampyr_spawn_chance > 100) + vampyr_spawn_chance = 100; } bool damage_undead_at(int pos, int damage) { @@ -607,13 +651,15 @@ bool damage_undead_at(int pos, int damage) { } if (undeads[i].undead_type == "undead_resident") { undead_residents_count--; - if (undead_residents_count < 0) undead_residents_count = 0; + if (undead_residents_count < 0) + undead_residents_count = 0; } if (undeads[i].sound_handle != -1) { p.destroy_sound(undeads[i].sound_handle); undeads[i].sound_handle = -1; } - play_creature_death_sounds("sounds/enemies/enemy_falls.ogg", undeads[i].voice_sound, x, pos, ZOMBIE_SOUND_VOLUME_STEP); + play_creature_death_sounds("sounds/enemies/enemy_falls.ogg", undeads[i].voice_sound, x, pos, + ZOMBIE_SOUND_VOLUME_STEP); undeads.remove_at(i); } return true; @@ -623,9 +669,19 @@ bool damage_undead_at(int pos, int damage) { } // Backward compatibility aliases (to be removed after full refactoring) -Undead@[]@ zombies = @undeads; // Array alias for backward compatibility -Undead@ get_zombie_at(int pos) { return get_undead_at(pos); } -bool damage_zombie_at(int pos, int damage) { return damage_undead_at(pos, damage); } -void update_zombies() { update_undeads(); } -void clear_zombies() { clear_undeads(); } -void spawn_zombie() { spawn_undead("zombie"); } +Undead @[] @zombies = @undeads; // Array alias for backward compatibility +Undead @get_zombie_at(int pos) { + return get_undead_at(pos); +} +bool damage_zombie_at(int pos, int damage) { + return damage_undead_at(pos, damage); +} +void update_zombies() { + update_undeads(); +} +void clear_zombies() { + clear_undeads(); +} +void spawn_zombie() { + spawn_undead("zombie"); +} diff --git a/src/environment.nvgt b/src/environment.nvgt index 731df03..0d7ab86 100644 --- a/src/environment.nvgt +++ b/src/environment.nvgt @@ -8,7 +8,6 @@ void apply_falling_damage(int fall_height) { p.play_stationary("sounds/actions/hit_ground.ogg", false); if (fall_height <= SAFE_FALL_HEIGHT) { - speak_with_history("Landed safely.", true); return; } @@ -20,13 +19,18 @@ void apply_falling_damage(int fall_height) { // Apply damage player_health -= damage; - if (player_health < 0) player_health = 0; + if (player_health < 0) + player_health = 0; if (damage > 0) { play_player_damage_sound(); } // Feedback - speak_with_history("Fell " + fall_height + " feet! Took " + damage + " damage. " + player_health + " health remaining.", true); + dictionary fallArgs; + fallArgs.set("height", fall_height); + fallArgs.set("damage", damage); + fallArgs.set("health", player_health); + speak_with_history(trf("system.environment.fall.damage_report", fallArgs), true); } // Tree Object class Tree { @@ -74,7 +78,7 @@ class Tree { areaEnd = GRASS_END; } - Tree@ currentTree = @this; + Tree @currentTree = @ this; if (!place_tree_in_area(currentTree, areaStart, areaEnd)) { return; } @@ -111,10 +115,12 @@ class Tree { void try_regen() { // Skip if tree is fully stocked - if (!depleted && !is_chopped) return; + if (!depleted && !is_chopped) + return; // Check every minute (60000ms) - if (regen_timer.elapsed < 60000) return; + if (regen_timer.elapsed < 60000) + return; // Advance to next minute regen_timer.restart(); @@ -142,16 +148,21 @@ class Tree { // Determine base chance based on minutes elapsed int base_chance = 0; - if (minutes_since_depletion == 1) base_chance = 25; - else if (minutes_since_depletion == 2) base_chance = 50; - else if (minutes_since_depletion == 3) base_chance = 75; - else if (minutes_since_depletion == 4) base_chance = 100; + if (minutes_since_depletion == 1) + base_chance = 25; + else if (minutes_since_depletion == 2) + base_chance = 50; + else if (minutes_since_depletion == 3) + base_chance = 75; + else if (minutes_since_depletion == 4) + base_chance = 100; // Try to add items with decreasing probability int current_chance = base_chance; while (current_chance >= 25) { // Check if we can add anything - if (sticks >= 3 && vines >= 2) break; + if (sticks >= 3 && vines >= 2) + break; // Roll for success int roll = random(1, 100); @@ -183,8 +194,7 @@ class Tree { is_chopped = false; } } -} -Tree@[] trees; +} Tree @[] trees; const int SEARCH_POOL_STREAM_BANK = 0; const int SEARCH_POOL_FOREST = 1; @@ -198,7 +208,7 @@ class SearchPool { string[] terrain_tags; } -SearchPool@[] search_pools; +SearchPool @[] search_pools; int[] search_mass_noun_items; void init_search_pools() { @@ -209,26 +219,26 @@ void init_search_pools() { search_pools.resize(SEARCH_POOL_COUNT); @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].terrain_tags = { "stream_bank" }; + 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 = {"", ""}; + 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].terrain_tags = { "forest", "deep_forest" }; + 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 = {"", ""}; + 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].terrain_tags = { "gravel", "stone", "hard_stone" }; + 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 = {""}; + search_pools[SEARCH_POOL_STONE_TERRAIN].terrain_tags = {"gravel", "stone", "hard_stone"}; // Mass nouns for auto "Found X." fallback (no article). // Add new mass nouns here when adding search items that should not use "a/an". - search_mass_noun_items = { ITEM_CLAY }; + search_mass_noun_items = {ITEM_CLAY}; } string get_tree_area_terrain(int areaStart, int areaEnd) { @@ -258,7 +268,7 @@ int get_tree_max_for_area(int areaStart, int areaEnd) { return TREE_MAX_PER_AREA; } -bool get_tree_area_bounds_for_position(int pos, int &out areaStart, int &out areaEnd, string &out areaTerrain) { +bool get_tree_area_bounds_for_position(int pos, int& out areaStart, int& out areaEnd, string& out areaTerrain) { if (pos >= BASE_END + 1 && pos <= GRASS_END) { areaStart = BASE_END + 1; areaEnd = GRASS_END; @@ -271,12 +281,14 @@ bool get_tree_area_bounds_for_position(int pos, int &out areaStart, int &out are } int index = pos - expanded_area_start; - if (index < 0 || index >= int(expanded_terrain_types.length())) return false; + if (index < 0 || index >= int(expanded_terrain_types.length())) + return false; string terrain = expanded_terrain_types[index]; if (terrain.find("mountain:") == 0) { terrain = terrain.substr(9); } - if (terrain != "grass" && terrain != "forest" && terrain != "deep_forest") return false; + if (terrain != "grass" && terrain != "forest" && terrain != "deep_forest") + return false; int left = index; while (left > 0) { @@ -284,7 +296,8 @@ bool get_tree_area_bounds_for_position(int pos, int &out areaStart, int &out are if (leftTerrain.find("mountain:") == 0) { leftTerrain = leftTerrain.substr(9); } - if (leftTerrain != terrain) break; + if (leftTerrain != terrain) + break; left--; } @@ -295,7 +308,8 @@ bool get_tree_area_bounds_for_position(int pos, int &out areaStart, int &out are if (rightTerrain.find("mountain:") == 0) { rightTerrain = rightTerrain.substr(9); } - if (rightTerrain != terrain) break; + if (rightTerrain != terrain) + break; right++; } @@ -305,10 +319,11 @@ bool get_tree_area_bounds_for_position(int pos, int &out areaStart, int &out are return true; } -int count_trees_in_area(int areaStart, int areaEnd, Tree@ ignoreTree) { +int count_trees_in_area(int areaStart, int areaEnd, Tree @ignoreTree) { int count = 0; for (uint i = 0; i < trees.length(); i++) { - if (@trees[i] is ignoreTree) continue; + if (@trees[i] is ignoreTree) + continue; if (trees[i].position >= areaStart && trees[i].position <= areaEnd) { count++; } @@ -317,16 +332,19 @@ int count_trees_in_area(int areaStart, int areaEnd, Tree@ ignoreTree) { } bool is_near_required_climb(int pos, int radius) { - MountainRange@ mountain = get_mountain_at(pos); - if (mountain is null) return false; + MountainRange @mountain = get_mountain_at(pos); + if (mountain is null) + return false; int startPos = mountain.start_position; int endPos = mountain.end_position; int edgeStart = pos - radius - 1; int edgeEnd = pos + radius; - if (edgeStart < startPos) edgeStart = startPos; - if (edgeEnd > endPos - 1) edgeEnd = endPos - 1; + if (edgeStart < startPos) + edgeStart = startPos; + if (edgeEnd > endPos - 1) + edgeEnd = endPos - 1; for (int xPos = edgeStart; xPos <= edgeEnd; xPos++) { if (mountain.is_steep_section(xPos, xPos + 1)) { @@ -337,7 +355,7 @@ bool is_near_required_climb(int pos, int radius) { return false; } -bool tree_too_close_in_area(int pos, int areaStart, int areaEnd, Tree@ ignoreTree) { +bool tree_too_close_in_area(int pos, int areaStart, int areaEnd, Tree @ignoreTree) { // Keep trees away from the base edge if (pos < BASE_END + 5) { return true; @@ -348,8 +366,10 @@ bool tree_too_close_in_area(int pos, int areaStart, int areaEnd, Tree@ ignoreTre } for (uint i = 0; i < trees.length(); i++) { - if (@trees[i] is ignoreTree) continue; - if (trees[i].position < areaStart || trees[i].position > areaEnd) continue; + if (@trees[i] is ignoreTree) + continue; + if (trees[i].position < areaStart || trees[i].position > areaEnd) + continue; if (abs(trees[i].position - pos) < TREE_MIN_DISTANCE) { return true; @@ -358,7 +378,7 @@ bool tree_too_close_in_area(int pos, int areaStart, int areaEnd, Tree@ ignoreTre return false; } -bool place_tree_in_area(Tree@ tree, int areaStart, int areaEnd) { +bool place_tree_in_area(Tree @tree, int areaStart, int areaEnd) { int maxTrees = get_tree_max_for_area(areaStart, areaEnd); if (count_trees_in_area(areaStart, areaEnd, tree) >= maxTrees) { return false; @@ -388,7 +408,7 @@ bool spawn_tree_in_area(int areaStart, int areaEnd) { if (tree_too_close_in_area(pos, areaStart, areaEnd, null)) { continue; } - Tree@ t = Tree(pos); + Tree @t = Tree(pos); trees.insert_last(t); return true; } @@ -401,14 +421,15 @@ void spawn_trees(int grass_start, int grass_end) { } } -void get_tree_areas(int[]@ areaStarts, int[]@ areaEnds) { +void get_tree_areas(int[] @areaStarts, int[] @areaEnds) { areaStarts.resize(0); areaEnds.resize(0); areaStarts.insert_last(BASE_END + 1); areaEnds.insert_last(GRASS_END); - if (expanded_area_start == -1) return; + if (expanded_area_start == -1) + return; int total = int(expanded_terrain_types.length()); int index = 0; while (index < total) { @@ -423,7 +444,8 @@ void get_tree_areas(int[]@ areaStarts, int[]@ areaEnds) { if (nextTerrain.find("mountain:") == 0) { nextTerrain = nextTerrain.substr(9); } - if (nextTerrain != terrain) break; + if (nextTerrain != terrain) + break; index++; } int segmentEnd = index; @@ -434,10 +456,11 @@ void get_tree_areas(int[]@ areaStarts, int[]@ areaEnds) { } } -bool relocate_tree_to_any_area(Tree@ tree, int[]@ areaStarts, int[]@ areaEnds) { +bool relocate_tree_to_any_area(Tree @tree, int[] @areaStarts, int[] @areaEnds) { for (uint i = 0; i < areaStarts.length(); i++) { int maxTrees = get_tree_max_for_area(areaStarts[i], areaEnds[i]); - if (count_trees_in_area(areaStarts[i], areaEnds[i], tree) >= maxTrees) continue; + if (count_trees_in_area(areaStarts[i], areaEnds[i], tree) >= maxTrees) + continue; if (place_tree_in_area(tree, areaStarts[i], areaEnds[i])) { return true; } @@ -449,7 +472,8 @@ void normalize_tree_positions() { int[] areaStarts; int[] areaEnds; get_tree_areas(areaStarts, areaEnds); - if (areaStarts.length() == 0) return; + if (areaStarts.length() == 0) + return; for (uint i = 0; i < trees.length(); i++) { int areaStart = 0; @@ -480,7 +504,7 @@ void normalize_tree_positions() { while (areaTreeIndices.length() > maxTrees) { uint treeIndex = areaTreeIndices[areaTreeIndices.length() - 1]; - Tree@ tree = trees[treeIndex]; + Tree @tree = trees[treeIndex]; if (!relocate_tree_to_any_area(tree, areaStarts, areaEnds)) { if (tree.sound_handle != -1) { p.destroy_sound(tree.sound_handle); @@ -497,8 +521,8 @@ void normalize_tree_positions() { } if (areaTreeIndices.length() == 2) { - Tree@ firstTree = trees[areaTreeIndices[0]]; - Tree@ secondTree = trees[areaTreeIndices[1]]; + Tree @firstTree = trees[areaTreeIndices[0]]; + Tree @secondTree = trees[areaTreeIndices[1]]; if (abs(firstTree.position - secondTree.position) < TREE_MIN_DISTANCE) { if (!place_tree_in_area(secondTree, areaStart, areaEnd)) { place_tree_in_area(firstTree, areaStart, areaEnd); @@ -514,7 +538,7 @@ void update_environment() { get_active_audio_areas(areaStarts, areaEnds); bool limit_audio = (areaStarts.length() > 0); - for(uint i = 0; i < trees.length(); i++) { + for (uint i = 0; i < trees.length(); i++) { trees[i].try_regen(); if (limit_audio && !range_overlaps_active_areas(trees[i].position, trees[i].position, areaStarts, areaEnds)) { @@ -528,9 +552,9 @@ void update_environment() { } } -Tree@ get_tree_at(int target_x) { - for(uint i=0; i 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"); @@ -594,7 +624,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) { @@ -620,16 +650,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) { @@ -662,7 +685,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; } @@ -698,19 +721,18 @@ bool try_search_for_terrain(string terrain_type) { } for (uint j = 0; j < search_pools[i].terrain_tags.length(); j++) { if (search_pools[i].terrain_tags[j] == terrain_type) { - return try_find_weighted_resource(search_pools[i].item_types, - search_pools[i].weights, search_pools[i].found_messages); + return try_find_weighted_resource(search_pools[i].item_types, search_pools[i].weights, + search_pools[i].found_messages); } } } return false; } -void perform_search(int current_x) -{ +void perform_search(int current_x) { // First priority: Check for world drops on this tile or adjacent for (int check_x = current_x - 1; check_x <= current_x + 1; check_x++) { - WorldDrop@ drop = get_drop_at(check_x); + WorldDrop @drop = get_drop_at(check_x); if (drop != null) { if (!try_pickup_world_drop(drop)) { return; @@ -723,36 +745,39 @@ void perform_search(int current_x) // Check for snares nearby (adjacent within range) for (int check_x = current_x - SNARE_COLLECT_RANGE; check_x <= current_x + SNARE_COLLECT_RANGE; check_x++) { - // Skip current x? User said "beside". If on top, it breaks. + // Skip current x? User said "beside". If on top, it breaks. // But if I stand adjacent and shift... - if (check_x == current_x) continue; // Safety against collecting own snare you stand on? (Collision happens on move) + if (check_x == current_x) + continue; // Safety against collecting own snare you stand on? (Collision happens on move) // Actually, collision happens when *moving onto* it. If you placed it, you are on it. // If active is false (just placed), you can pick it up. // If active is true (you moved away), moving back breaks it. // So checking adjacent is correct. - - WorldSnare@ s = get_snare_at(check_x); + + WorldSnare @s = get_snare_at(check_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); personal_small_game_types.insert_last(s.catch_type); add_personal_count(ITEM_SNARES, 1); // Recover snare - speak_with_history("Collected " + s.catch_type + " and snare.", true); + dictionary collectedSnareArgs; + collectedSnareArgs.set("catch_type", i18n_translate_fragment_value(s.catch_type)); + speak_with_history(trf("system.environment.snare.collected_with_catch", collectedSnareArgs), 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 - speak_with_history("Collected snare.", true); + speak_with_history(tr("system.environment.snare.collected_empty"), true); } string collectSoundItem = s.has_catch ? s.catch_type : "snare"; play_item_collect_sound(collectSoundItem); @@ -762,7 +787,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; } @@ -791,7 +816,8 @@ void perform_search(int current_x) break; } } - if (near_stream_bank) break; + if (near_stream_bank) + break; } } @@ -803,17 +829,15 @@ void perform_search(int current_x) // Trees (Sticks/Vines) - Check for nearby tree, but only on non-stone terrain // Skip tree search if player is on stone terrain (prioritize stone finding) string search_terrain = get_terrain_at_position(current_x); - bool skip_tree_search = (search_terrain == "stone" || search_terrain == "gravel" || - search_terrain == "snow" || search_terrain == "hard_stone"); + bool skip_tree_search = (search_terrain == "stone" || search_terrain == "gravel" || search_terrain == "snow" || + search_terrain == "hard_stone"); - Tree@ nearest = null; + Tree @nearest = null; int nearest_distance = 999; if (!skip_tree_search) { - for(uint i=0; i 0 || nearest.vines > 0) - { + + if (nearest.sticks > 0 || nearest.vines > 0) { bool find_stick = (nearest.vines <= 0) || (nearest.sticks > 0 && random(0, 1) == 0); bool took_item = false; @@ -845,13 +867,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 { @@ -859,37 +881,35 @@ 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; } - - if(nearest.sticks == 0 && nearest.vines == 0) { + + if (nearest.sticks == 0 && nearest.vines == 0) { nearest.depleted = true; nearest.regen_timer.restart(); nearest.minutes_since_depletion = 0; } - } - else - { - speak_with_history("This area has nothing left.", true); + } else { + speak_with_history(tr("system.environment.area.nothing_left"), true); } return; } @@ -898,12 +918,12 @@ void perform_search(int current_x) return; } - speak_with_history("Found nothing.", true); + speak_with_history(tr("system.search.found_nothing"), true); } // Climbing functions void start_climbing_tree(int target_x) { - Tree@ tree = get_tree_at(target_x); + Tree @tree = get_tree_at(target_x); if (tree == null || tree.is_chopped) { return; } @@ -912,11 +932,14 @@ void start_climbing_tree(int target_x) { climbing = true; climb_target_y = ground_elevation + tree.height; climb_timer.restart(); - speak_with_history("Started climbing tree. Height is " + tree.height + " feet.", true); + dictionary climbTreeArgs; + climbTreeArgs.set("height", tree.height); + speak_with_history(trf("system.environment.tree.climb_started", climbTreeArgs), true); } void update_climbing() { - if (!climbing) return; + if (!climbing) + return; if (y == climb_target_y) { climbing = false; return; @@ -935,7 +958,9 @@ void update_climbing() { climbing = false; int ground_elevation = get_mountain_elevation_at(x); int height_above_ground = y - ground_elevation; - speak_with_history("Reached the top at " + height_above_ground + " feet.", true); + dictionary reachedTopArgs; + reachedTopArgs.set("height", height_above_ground); + speak_with_history(trf("system.environment.tree.climb_reached_top", reachedTopArgs), true); attempt_pet_offer_from_tree(); } } @@ -947,7 +972,7 @@ void update_climbing() { if (y <= climb_target_y) { climbing = false; y = climb_target_y; - speak_with_history("Safely reached the ground.", true); + speak_with_history(tr("system.environment.tree.climb_reached_ground"), true); } } } @@ -955,16 +980,18 @@ void update_climbing() { void climb_down_tree() { int ground_elevation = get_mountain_elevation_at(x); - if (y == ground_elevation || climbing) return; + if (y == ground_elevation || climbing) + return; climbing = true; climb_target_y = ground_elevation; climb_timer.restart(); - speak_with_history("Climbing down.", true); + speak_with_history(tr("system.environment.tree.climb_down"), true); } void start_falling() { - if (y <= 0 || falling) return; + if (y <= 0 || falling) + return; falling = true; fall_start_y = y; // Remember where we started falling from @@ -975,7 +1002,8 @@ void start_falling() { } void update_falling() { - if (!falling) return; + if (!falling) + return; // Get ground level (mountain elevation or 0) int ground_level = get_mountain_elevation_at(x); @@ -995,8 +1023,10 @@ void update_falling() { // Pitch ranges from 100 (high up) to 50 (near ground) float height_above_ground = float(y - ground_level); float pitch_percent = 50.0 + (50.0 * (height_above_ground / 30.0)); - if (pitch_percent < 50.0) pitch_percent = 50.0; - if (pitch_percent > 100.0) pitch_percent = 100.0; + if (pitch_percent < 50.0) + pitch_percent = 50.0; + if (pitch_percent > 100.0) + pitch_percent = 100.0; fall_sound_handle = p.play_stationary_extended("sounds/actions/falling.ogg", true, 0, 0, 0, pitch_percent); @@ -1028,7 +1058,7 @@ void land_on_ground(int ground_level) { // Mountain movement check bool can_move_mountain(int from_x, int to_x) { - MountainRange@ mountain = get_mountain_at(to_x); + MountainRange @mountain = get_mountain_at(to_x); if (mountain is null) { // Not entering a mountain return true; @@ -1044,16 +1074,16 @@ bool can_move_mountain(int from_x, int to_x) { if (mountain.is_steep_section(from_x, to_x)) { // Need rope if (get_personal_count(ITEM_ROPES) < 1) { - speak_movement_blocked("You'll need a rope to climb there."); + speak_movement_blocked(tr("system.environment.movement.need_rope")); return false; } // Prompt for rope climb int elevation_change = mountain.get_elevation_change(from_x, to_x); if (elevation_change > 0) { - speak_movement_blocked("Press up to climb up."); + speak_movement_blocked(tr("system.environment.movement.press_up_climb")); } else { - speak_movement_blocked("Press down to climb down."); + speak_movement_blocked(tr("system.environment.movement.press_down_climb")); } // Store pending rope climb info @@ -1066,9 +1096,11 @@ bool can_move_mountain(int from_x, int to_x) { } bool can_enter_stream_tile(int pos) { - if (!is_deep_stream_at(pos)) return true; - if (get_personal_count(ITEM_CANOES) > 0) return true; - speak_movement_blocked("You need a canoe to cross deep water."); + if (!is_deep_stream_at(pos)) + return true; + if (get_personal_count(ITEM_CANOES) > 0) + return true; + speak_movement_blocked(tr("system.environment.movement.need_canoe")); return false; } @@ -1109,14 +1141,19 @@ void start_rope_climb(bool climbing_up, int target_x, int target_elevation) { // Calculate distance to climb (use actual distance from current position) int distance = abs(elevation_diff); - string direction = rope_climb_up ? "up" : "down"; - speak_with_history("Climbing " + direction + ". " + distance + " feet.", true); + string direction = rope_climb_up ? tr("system.environment.rope_climb.direction.up") + : tr("system.environment.rope_climb.direction.down"); + dictionary ropeClimbArgs; + ropeClimbArgs.set("direction", direction); + ropeClimbArgs.set("distance", distance); + speak_with_history(trf("system.environment.rope_climb.progress", ropeClimbArgs), true); rope_climb_sound_handle = -1; } void update_rope_climbing() { - if (!rope_climbing) return; + if (!rope_climbing) + return; // Check if we're already at the target (shouldn't happen, but safety check) if (y == rope_climb_target_y) { @@ -1164,11 +1201,14 @@ void complete_rope_climb() { // Play footstep for new terrain play_footstep(x, BASE_END, GRASS_END); - speak_with_history("Reached elevation " + y + ".", true); + dictionary elevationArgs; + elevationArgs.set("elevation", y); + speak_with_history(trf("system.environment.rope_climb.reached_elevation", elevationArgs), true); } void check_rope_climb_fall() { - if (!rope_climbing) return; + if (!rope_climbing) + return; if (key_down(KEY_LEFT) || key_down(KEY_RIGHT)) { rope_climb_sound_handle = -1; diff --git a/src/fishing.nvgt b/src/fishing.nvgt index dc29803..6d6291f 100644 --- a/src/fishing.nvgt +++ b/src/fishing.nvgt @@ -51,7 +51,7 @@ void reset_fishing_session() { fishing_checks_done = 0; } -bool get_nearby_stream(int pos, int range, int &out stream_start, int &out stream_end) { +bool get_nearby_stream(int pos, int range, int& out stream_start, int& out stream_end) { int best_distance = range + 1; // Check regular world streams @@ -89,14 +89,18 @@ bool get_nearby_stream(int pos, int range, int &out stream_start, int &out strea } int get_cast_target_position(int origin_x, int stream_start, int stream_end) { - if (origin_x < stream_start) return stream_start; - if (origin_x > stream_end) return stream_end; + if (origin_x < stream_start) + return stream_start; + if (origin_x > stream_end) + return stream_end; return (facing == 1) ? stream_end : stream_start; } int get_random_stream_tile() { - if (target_stream_start < 0 || target_stream_end < 0) return x; - if (target_stream_end < target_stream_start) return target_stream_start; + if (target_stream_start < 0 || target_stream_end < 0) + return x; + if (target_stream_end < target_stream_start) + return target_stream_start; return random(target_stream_start, target_stream_end); } @@ -105,10 +109,14 @@ string get_random_fish_type() { if (is_night) { int roll = random(0, 99); - if (roll < 40) return "catfish"; - else if (roll < 60) return "trout"; - else if (roll < 80) return "bass"; - else return "salmon"; + if (roll < 40) + return "catfish"; + else if (roll < 60) + return "trout"; + else if (roll < 80) + return "bass"; + else + return "salmon"; } return fish_types[random(0, 3)]; @@ -116,10 +124,13 @@ string get_random_fish_type() { string get_fish_size_label(int weight) { int clamped = clamp_fish_weight(weight); - if (clamped <= 7) return "small"; - if (clamped <= 17) return "medium sized"; - if (clamped <= 27) return "large"; - return "monster"; + if (clamped <= 7) + return tr("system.fishing.size.small"); + if (clamped <= 17) + return tr("system.fishing.size.medium"); + if (clamped <= 27) + return tr("system.fishing.size.large"); + return tr("system.fishing.size.monster"); } void break_fishing_pole(string message) { @@ -131,7 +142,7 @@ void break_fishing_pole(string message) { reset_fishing_session(); if (message == "") { - message = "Your fishing pole broke."; + message = tr("system.fishing.pole_broke.default"); } speak_with_history(message, true); } @@ -144,16 +155,17 @@ void lose_fish(string message) { } void start_casting() { - if (is_casting || line_in_water || fish_on_line || is_reeling) return; + if (is_casting || line_in_water || fish_on_line || is_reeling) + return; if (!fishing_pole_equipped) { - speak_with_history("You need a fishing pole equipped.", true); + speak_with_history(tr("system.fishing.require_pole_equipped"), true); return; } int stream_start = -1; int stream_end = -1; if (!get_nearby_stream(x, FISHING_NEAR_STREAM_RANGE, stream_start, stream_end)) { - speak_with_history("You need to be within 2 tiles of a stream.", true); + speak_with_history(tr("system.fishing.require_near_stream"), true); return; } @@ -170,11 +182,14 @@ void start_casting() { } void update_casting() { - if (!is_casting) return; - if (cast_move_timer.elapsed < FISHING_CAST_MOVE_MS) return; + if (!is_casting) + return; + if (cast_move_timer.elapsed < FISHING_CAST_MOVE_MS) + return; cast_move_timer.restart(); - if (cast_direction == 0) cast_direction = (facing == 1) ? 1 : -1; + if (cast_direction == 0) + cast_direction = (facing == 1) ? 1 : -1; cast_position += cast_direction; int offset = cast_position - cast_origin_x; @@ -194,11 +209,13 @@ void update_casting() { cast_direction = -1; } - play_1d_with_volume_step("sounds/actions/cast_strength.ogg", x, cast_position, false, PLAYER_WEAPON_SOUND_VOLUME_STEP); + play_1d_with_volume_step("sounds/actions/cast_strength.ogg", x, cast_position, false, + PLAYER_WEAPON_SOUND_VOLUME_STEP); } void release_cast() { - if (!is_casting) return; + if (!is_casting) + return; is_casting = false; stop_fishing_sound(); @@ -214,7 +231,7 @@ void release_cast() { p.play_stationary("sounds/actions/bad_cast.ogg", false); line_position = -1; if (random(1, 100) <= FISHING_BAD_CAST_BREAK_CHANCE) { - break_fishing_pole("Your fishing pole broke."); + break_fishing_pole(tr("system.fishing.pole_broke.default")); return; } } @@ -225,7 +242,8 @@ void release_cast() { } void update_waiting_for_fish() { - if (!line_in_water) return; + if (!line_in_water) + return; const int check_interval = 1000; int checks_ready = fishing_timer.elapsed / check_interval; @@ -233,7 +251,8 @@ void update_waiting_for_fish() { while (fishing_checks_done < checks_ready) { fishing_checks_done++; catch_chance = FISHING_CATCH_CHANCE_START + (fishing_checks_done - 1) * FISHING_CATCH_CHANCE_STEP; - if (catch_chance > FISHING_CATCH_CHANCE_MAX) catch_chance = FISHING_CATCH_CHANCE_MAX; + if (catch_chance > FISHING_CATCH_CHANCE_MAX) + catch_chance = FISHING_CATCH_CHANCE_MAX; if (catch_chance > 0 && random(1, 100) <= catch_chance) { line_in_water = false; @@ -241,14 +260,15 @@ void update_waiting_for_fish() { reel_position = (line_position >= 0) ? line_position : get_random_stream_tile(); reel_direction = 0; hooked_fish_type = get_random_fish_type(); - speak_with_history("A fish is on the line!", true); + speak_with_history(tr("system.fishing.fish_on_line"), true); break; } } } void start_reeling() { - if (!fish_on_line || is_reeling) return; + if (!fish_on_line || is_reeling) + return; if (reel_position < 0) { reel_position = (line_position >= 0) ? line_position : get_random_stream_tile(); } @@ -257,12 +277,15 @@ void start_reeling() { reel_direction = reel_start_direction; cast_move_timer.restart(); stop_fishing_sound(); - play_1d_with_volume_step("sounds/actions/cast_strength.ogg", x, reel_position, false, PLAYER_WEAPON_SOUND_VOLUME_STEP); + play_1d_with_volume_step("sounds/actions/cast_strength.ogg", x, reel_position, false, + PLAYER_WEAPON_SOUND_VOLUME_STEP); } void update_reeling() { - if (!is_reeling) return; - if (cast_move_timer.elapsed < FISHING_CAST_MOVE_MS) return; + if (!is_reeling) + return; + if (cast_move_timer.elapsed < FISHING_CAST_MOVE_MS) + return; cast_move_timer.restart(); reel_position += reel_direction; @@ -276,15 +299,18 @@ void update_reeling() { reel_direction = 1; } - if (reel_position < 0) reel_position = 0; - if (reel_position > MAP_SIZE - 1) reel_position = MAP_SIZE - 1; + if (reel_position < 0) + reel_position = 0; + if (reel_position > MAP_SIZE - 1) + reel_position = MAP_SIZE - 1; - play_1d_with_volume_step("sounds/actions/cast_strength.ogg", x, reel_position, false, PLAYER_WEAPON_SOUND_VOLUME_STEP); + play_1d_with_volume_step("sounds/actions/cast_strength.ogg", x, reel_position, false, + PLAYER_WEAPON_SOUND_VOLUME_STEP); } 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; } @@ -300,19 +326,27 @@ void catch_fish() { play_item_collect_sound(collectSoundItem); string fish_name = hooked_fish_type; - if (fish_name == "") fish_name = "fish"; - speak_with_history("Caught a " + size_label + " " + fish_name + ".", true); + if (fish_name == "") + fish_name = i18n_translate_fragment_value("fish"); + else + fish_name = i18n_translate_fragment_value(fish_name); + dictionary catchArgs; + catchArgs.set("size", size_label); + catchArgs.set("fish", fish_name); + speak_with_history(trf("system.fishing.caught", catchArgs), true); reset_fishing_session(); } void release_reel() { - if (!is_reeling) return; + if (!is_reeling) + return; stop_fishing_sound(); is_reeling = false; - if (!fish_on_line) return; + if (!fish_on_line) + return; if (reel_position == x) { catch_fish(); return; @@ -320,9 +354,9 @@ void release_reel() { if (is_any_water_at(reel_position)) { if (random(1, 100) <= FISHING_EARLY_ESCAPE_CHANCE) { - lose_fish("The fish slipped off in the water."); + lose_fish(tr("system.fishing.fish_slipped_off_water")); } else { - speak_with_history("The fish is still on the line.", true); + speak_with_history(tr("system.fishing.fish_still_on_line"), true); } return; } @@ -331,15 +365,15 @@ void release_reel() { reel_start_direction = (reel_position < x) ? 1 : -1; } - bool before_player = (reel_start_direction == 1 && reel_position < x) || - (reel_start_direction == -1 && reel_position > x); - bool past_player = (reel_start_direction == 1 && reel_position > x) || - (reel_start_direction == -1 && reel_position < x); + bool before_player = + (reel_start_direction == 1 && reel_position < x) || (reel_start_direction == -1 && reel_position > x); + bool past_player = + (reel_start_direction == 1 && reel_position > x) || (reel_start_direction == -1 && reel_position < x); if (past_player) { p.play_stationary("sounds/actions/bad_cast.ogg", false); if (random(1, 100) <= FISHING_LATE_ESCAPE_CHANCE) { - lose_fish("The fish got away."); + lose_fish(tr("system.fishing.fish_got_away")); } else { catch_fish(); } @@ -348,27 +382,28 @@ void release_reel() { if (before_player) { if (random(1, 100) <= FISHING_EARLY_ESCAPE_CHANCE) { - lose_fish("The fish got away."); + lose_fish(tr("system.fishing.fish_got_away")); } else { - speak_with_history("The fish is still on the line.", true); + speak_with_history(tr("system.fishing.fish_still_on_line"), true); } return; } } bool handle_fishing_breaks() { - if (!line_in_water && !fish_on_line && !is_reeling) return false; + if (!line_in_water && !fish_on_line && !is_reeling) + return false; if (!fishing_pole_equipped) { - break_fishing_pole("You switched weapons and your fishing pole broke."); + break_fishing_pole(tr("system.fishing.pole_broke.switched_weapons")); return true; } if (key_down(KEY_LEFT) || key_down(KEY_RIGHT) || key_pressed(KEY_UP) || jumping) { if (fish_on_line || is_reeling) { - break_fishing_pole("You moved and the fish got away. Your fishing pole broke."); + break_fishing_pole(tr("system.fishing.pole_broke.moved_fish_got_away")); } else { - break_fishing_pole("You moved and your fishing pole broke."); + break_fishing_pole(tr("system.fishing.pole_broke.moved")); } return true; } @@ -380,7 +415,8 @@ void update_fishing() { bool ctrl_down = is_ctrl_down(); bool ctrl_pressed = is_ctrl_pressed(); - if (handle_fishing_breaks()) return; + if (handle_fishing_breaks()) + return; if (fishing_pole_equipped && ctrl_pressed && !is_casting && !line_in_water && !fish_on_line && !is_reeling) { start_casting(); @@ -400,7 +436,7 @@ void update_fishing() { catch_chance = 0; fishing_checks_done = 0; fishing_timer.restart(); - speak_with_history("You stop fishing.", true); + speak_with_history(tr("system.fishing.stopped"), true); } else { update_waiting_for_fish(); } diff --git a/src/flying_creature_template.nvgt b/src/flying_creature_template.nvgt index c5b1baa..d646290 100644 --- a/src/flying_creature_template.nvgt +++ b/src/flying_creature_template.nvgt @@ -67,5 +67,5 @@ // add_personal_count(ITEM_MEAT, 1); // add_personal_count(ITEM_FEATHERS, random(2, 4)); // add_personal_count(ITEM_DOWN, random(1, 2)); -// speak_with_history("Butchered duck. Got 1 meat, feathers, and down.", true); +// speak_with_history(tr("system.crafting.butcher.duck_example"), true); // } diff --git a/src/fylgja_system.nvgt b/src/fylgja_system.nvgt index 2e85675..26f2565 100644 --- a/src/fylgja_system.nvgt +++ b/src/fylgja_system.nvgt @@ -6,24 +6,15 @@ const int FYLGJA_STAGE_COUNT = 9; const int FYLGJA_UNICORN = 0; const int UNICORN_TRAMPLE_DAMAGE = 20; -string[] fylgjaStageNames = { - "tenuous", - "faint", - "stirring", - "budding", - "kindled", - "bound", - "sworn", - "ascendant", - "ultimate" -}; +string[] fylgjaStageNames = {"tenuous", "faint", "stirring", "budding", "kindled", + "bound", "sworn", "ascendant", "ultimate"}; int[] adventureIds = {ADVENTURE_UNICORN}; -string[] adventureStageTargets = {"unicorn"}; +string[] adventureStageTargetKeys = {"unicorn"}; int[] adventureCompletionCounts = {0}; int[] fylgjaAdventureIds = {ADVENTURE_UNICORN}; -string[] fylgjaNames = {"Unicorn"}; +string[] fylgjaNameKeys = {"unicorn"}; int lastFylgjaDay = -1; bool fylgjaCharging = false; @@ -49,67 +40,86 @@ void reset_fylgja_state() { int get_adventure_index(int adventureId) { for (uint i = 0; i < adventureIds.length(); i++) { - if (adventureIds[i] == adventureId) return int(i); + if (adventureIds[i] == adventureId) + return int(i); } return -1; } int get_fylgja_index_for_adventure(int adventureId) { for (uint i = 0; i < fylgjaAdventureIds.length(); i++) { - if (fylgjaAdventureIds[i] == adventureId) return int(i); + if (fylgjaAdventureIds[i] == adventureId) + return int(i); } return -1; } bool is_fylgja_unlocked(int fylgjaIndex) { - if (fylgjaIndex < 0 || fylgjaIndex >= int(fylgjaAdventureIds.length())) return false; + if (fylgjaIndex < 0 || fylgjaIndex >= int(fylgjaAdventureIds.length())) + return false; int adventureIndex = get_adventure_index(fylgjaAdventureIds[fylgjaIndex]); - if (adventureIndex < 0) return false; + if (adventureIndex < 0) + return false; return adventureCompletionCounts[adventureIndex] >= FYLGJA_STAGE_COUNT; } int get_unlocked_fylgja_count() { int unlockedCount = 0; - for (uint i = 0; i < fylgjaNames.length(); i++) { - if (is_fylgja_unlocked(int(i))) unlockedCount++; + for (uint i = 0; i < fylgjaNameKeys.length(); i++) { + if (is_fylgja_unlocked(int(i))) + unlockedCount++; } return unlockedCount; } -void append_adventure_completion_rewards(int adventureId, string[]@ rewards) { +void append_adventure_completion_rewards(int adventureId, string[] @rewards) { int adventureIndex = get_adventure_index(adventureId); - if (adventureIndex < 0 || @rewards == null) return; + if (adventureIndex < 0 || @rewards == null) + return; adventureCompletionCounts[adventureIndex]++; int completionCount = adventureCompletionCounts[adventureIndex]; int stageIndex = completionCount - 1; - if (stageIndex < 0) stageIndex = 0; - if (stageIndex >= int(fylgjaStageNames.length())) stageIndex = int(fylgjaStageNames.length()) - 1; + if (stageIndex < 0) + stageIndex = 0; + if (stageIndex >= int(fylgjaStageNames.length())) + stageIndex = int(fylgjaStageNames.length()) - 1; string stageName = fylgjaStageNames[stageIndex]; - string targetName = adventureStageTargets[adventureIndex]; - rewards.insert_last("You have a " + stageName + " connection with the " + targetName + "."); + string stageLabel = tr("system.fylgja.stage." + stageName); + string targetKey = adventureStageTargetKeys[adventureIndex]; + string targetLabel = tr("system.fylgja.target." + targetKey); + dictionary connectionArgs; + connectionArgs.set("stage", stageLabel); + connectionArgs.set("target", targetLabel); + rewards.insert_last(trf("system.fylgja.connection_with_target", connectionArgs)); int fylgjaIndex = get_fylgja_index_for_adventure(adventureId); - if (fylgjaIndex == -1) return; + if (fylgjaIndex == -1) + return; if (completionCount >= FYLGJA_STAGE_COUNT) { + dictionary unlockedArgs; + unlockedArgs.set("name", tr("system.fylgja.name." + fylgjaNameKeys[fylgjaIndex])); if (completionCount == FYLGJA_STAGE_COUNT) { - rewards.insert_last("You have unlocked the " + fylgjaNames[fylgjaIndex] + " Fylgja!"); + rewards.insert_last(trf("system.fylgja.unlocked", unlockedArgs)); } else { - rewards.insert_last("You have already unlocked the " + fylgjaNames[fylgjaIndex] + " Fylgja."); + rewards.insert_last(trf("system.fylgja.already_unlocked", unlockedArgs)); } } } void check_fylgja_menu() { - if (!key_pressed(KEY_F)) return; - if (fylgjaCharging) return; - if (get_unlocked_fylgja_count() == 0) return; + if (!key_pressed(KEY_F)) + return; + if (fylgjaCharging) + return; + if (get_unlocked_fylgja_count() == 0) + return; if (lastFylgjaDay == current_day) { - speak_with_history("You have already used your Fylgja today.", true); + speak_with_history(tr("system.fylgja.already_used_today"), true); return; } @@ -120,16 +130,19 @@ void run_fylgja_menu() { string[] options; int[] fylgjaIndices; - for (uint i = 0; i < fylgjaNames.length(); i++) { + for (uint i = 0; i < fylgjaNameKeys.length(); i++) { if (is_fylgja_unlocked(int(i))) { - options.insert_last(fylgjaNames[i] + " Fylgja"); + dictionary optionArgs; + optionArgs.set("name", tr("system.fylgja.name." + fylgjaNameKeys[i])); + options.insert_last(trf("system.fylgja.option", optionArgs)); fylgjaIndices.insert_last(int(i)); } } - if (options.length() == 0) return; + if (options.length() == 0) + return; - speak_with_history("Fylgja menu.", true); + speak_with_history(tr("system.fylgja.menu.prompt"), true); int selection = 0; speak_with_history(options[selection], true); @@ -147,14 +160,16 @@ void run_fylgja_menu() { if (key_pressed(KEY_DOWN)) { play_menu_move_sound(); selection++; - if (selection >= int(options.length())) selection = 0; + if (selection >= int(options.length())) + selection = 0; speak_with_history(options[selection], true); } if (key_pressed(KEY_UP)) { play_menu_move_sound(); selection--; - if (selection < 0) selection = int(options.length()) - 1; + if (selection < 0) + selection = int(options.length()) - 1; speak_with_history(options[selection], true); } @@ -172,7 +187,7 @@ void run_fylgja_menu() { bool activate_fylgja(int fylgjaIndex) { if (fylgjaIndex == FYLGJA_UNICORN) { if (!can_start_unicorn_fylgja_charge()) { - speak_with_history("You need open ground in front of you to charge.", true); + speak_with_history(tr("system.fylgja.need_open_ground_to_charge"), true); return false; } start_unicorn_fylgja_charge(); @@ -182,11 +197,14 @@ bool activate_fylgja(int fylgjaIndex) { } bool can_start_unicorn_fylgja_charge() { - if (x <= BASE_END) return false; + if (x <= BASE_END) + return false; int step = (facing == 1) ? 1 : -1; int targetX = x + step; - if (targetX < 0 || targetX >= MAP_SIZE) return false; - if (should_stop_charge_for_climb_up(x, targetX)) return false; + if (targetX < 0 || targetX >= MAP_SIZE) + return false; + if (should_stop_charge_for_climb_up(x, targetX)) + return false; return true; } @@ -225,8 +243,9 @@ void start_unicorn_fylgja_charge() { } bool should_stop_charge_for_climb_up(int fromX, int toX) { - MountainRange@ mountain = get_mountain_at(toX); - if (mountain is null) return false; + MountainRange @mountain = get_mountain_at(toX); + if (mountain is null) + return false; int elevationChange = mountain.get_elevation_change(fromX, toX); return elevationChange >= MOUNTAIN_STEEP_THRESHOLD; @@ -248,7 +267,8 @@ string get_unicorn_charge_sound(int posX) { void update_fylgja_charge_audio() { string soundFile = get_unicorn_charge_sound(x); - bool needNewSound = (fylgjaSoundHandle == -1 || !p.sound_is_active(fylgjaSoundHandle) || currentFylgjaSound != soundFile); + bool needNewSound = + (fylgjaSoundHandle == -1 || !p.sound_is_active(fylgjaSoundHandle) || currentFylgjaSound != soundFile); if (needNewSound) { if (fylgjaSoundHandle != -1) { @@ -272,7 +292,8 @@ void stop_fylgja_charge() { } void update_fylgja_charge() { - if (!fylgjaCharging) return; + if (!fylgjaCharging) + return; if (key_down(KEY_LSHIFT) || key_down(KEY_RSHIFT)) { stop_fylgja_charge(); diff --git a/src/i18n.nvgt b/src/i18n.nvgt new file mode 100644 index 0000000..8275cc4 --- /dev/null +++ b/src/i18n.nvgt @@ -0,0 +1,778 @@ +// 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 = ""; + string fallbackValue = value; + if (!i18n_catalog_get_string(i18nExactEnglishToKey, value, key)) { + if (value.find_first("_") < 0) + return value; + string normalized = value; + normalized.replace("_", " "); + fallbackValue = normalized; + if (!i18n_catalog_get_string(i18nExactEnglishToKey, normalized, key)) + return value; + } + + return i18n_lookup_key_with_fallback(key, fallbackValue); +} + +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/intro.nvgt b/src/intro.nvgt new file mode 100644 index 0000000..7efa4f2 --- /dev/null +++ b/src/intro.nvgt @@ -0,0 +1,84 @@ +#include "libstorm-nvgt/character_dialog.nvgt" + +bool story_dialog_configured = false; +string[] story_voice_god_keys = {"system.story.god.odin", "system.story.god.thor", "system.story.god.freyja", + "system.story.god.loki", "system.story.god.tyr", "system.story.god.baldur", + "system.story.god.frigg", "system.story.god.heimdall", "system.story.god.hel", + "system.story.god.fenrir", "system.story.god.freyr"}; + +void story_dialog_speak(const string& in message, bool interrupt) { + speak_with_history(message, interrupt); +} + +string get_random_story_voice_god() { + if (story_voice_god_keys.length() == 0) { + return tr("system.story.god.fallback"); + } + + int index = random(0, story_voice_god_keys.length() - 1); + return tr(story_voice_god_keys[index]); +} + +string format_story_voice_line(const string& in god_name, const string& in line) { + dictionary args; + args.set("speaker", god_name); + args.set("line", line); + return trf("system.story.voice.format", args); +} + +void configure_story_dialog_if_needed() { + if (story_dialog_configured) { + return; + } + + character_dialog_set_speak_callback(story_dialog_speak); + character_dialog_set_sound_path("sounds/dialog"); + character_dialog_set_show_usage_instructions(true); + story_dialog_configured = true; +} + +void run_story_dialog() { + configure_story_dialog_if_needed(); + character_dialog_reset_usage_instructions(); + + string[] story_lines; + story_lines.insert_last(tr("system.story.line01")); + story_lines.insert_last(tr("system.story.line02")); + story_lines.insert_last(tr("system.story.line03")); + story_lines.insert_last(tr("system.story.line04")); + story_lines.insert_last(tr("system.story.line05")); + story_lines.insert_last(tr("system.story.line06")); + story_lines.insert_last(tr("system.story.line07")); + story_lines.insert_last(tr("system.story.line08")); + story_lines.insert_last(tr("system.story.line09")); + story_lines.insert_last(tr("system.story.line10")); + story_lines.insert_last(tr("system.story.line11")); + story_lines.insert_last(tr("system.story.line12")); + story_lines.insert_last(tr("system.story.line13")); + story_lines.insert_last(tr("system.story.line14")); + story_lines.insert_last(tr("system.story.line15")); + story_lines.insert_last(tr("system.story.line16")); + story_lines.insert_last(tr("system.story.line17")); + story_lines.insert_last(tr("system.story.line18")); + story_lines.insert_last(tr("system.story.line19")); + story_lines.insert_last(tr("system.story.line20")); + story_lines.insert_last(tr("system.story.line21")); + story_lines.insert_last(tr("system.story.line22")); + story_lines.insert_last(tr("system.story.line23")); + story_lines.insert_last(tr("system.story.line24")); + story_lines.insert_last(tr("system.story.line25")); + story_lines.insert_last(tr("system.story.line26")); + story_lines.insert_last(tr("system.story.line27")); + story_lines.insert_last(tr("system.story.line28")); + story_lines.insert_last(format_story_voice_line(get_random_story_voice_god(), tr("system.story.line29"))); + story_lines.insert_last(format_story_voice_line(get_random_story_voice_god(), tr("system.story.line30"))); + story_lines.insert_last(format_story_voice_line(tr("system.story.god.hel"), tr("system.story.line31"))); + story_lines.insert_last(format_story_voice_line(get_random_story_voice_god(), tr("system.story.line32"))); + story_lines.insert_last(format_story_voice_line(get_random_story_voice_god(), tr("system.story.line33"))); + story_lines.insert_last(format_story_voice_line(get_random_story_voice_god(), tr("system.story.line34"))); + story_lines.insert_last(format_story_voice_line(get_random_story_voice_god(), tr("system.story.line35"))); + story_lines.insert_last(tr("system.story.line36")); + story_lines.insert_last(tr("system.story.line37")); + + character_dialog_show_lines(story_lines, true); +} diff --git a/src/inventory_items.nvgt b/src/inventory_items.nvgt index a6b09f4..d0c6e4a 100644 --- a/src/inventory_items.nvgt +++ b/src/inventory_items.nvgt @@ -70,8 +70,10 @@ int get_personal_stack_limit() { } int get_storage_stack_limit() { - if (storage_level <= STORAGE_LEVEL_BASE) return BASE_STORAGE_START_MAX; - if (storage_level == STORAGE_LEVEL_UPGRADE_1) return BASE_STORAGE_UPGRADE_1_MAX; + if (storage_level <= STORAGE_LEVEL_BASE) + return BASE_STORAGE_START_MAX; + if (storage_level == STORAGE_LEVEL_UPGRADE_1) + return BASE_STORAGE_UPGRADE_1_MAX; return BASE_STORAGE_UPGRADE_2_MAX; } @@ -79,58 +81,63 @@ int get_arrow_limit() { // Quiver required to hold arrows // Each quiver holds 12 arrows int quivers = get_personal_count(ITEM_QUIVERS); - if (quivers == 0) return 0; + if (quivers == 0) + return 0; return quivers * ARROW_CAPACITY_PER_QUIVER; } void clamp_arrows_to_quiver_limit() { int maxArrows = get_arrow_limit(); int currentArrows = get_personal_count(ITEM_ARROWS); - if (currentArrows <= maxArrows) return; + if (currentArrows <= maxArrows) + return; 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); + dictionary arrowArgs; + arrowArgs.set("max", maxArrows); + speak_with_history(trf("system.inventory.max_arrows_with_quivers", arrowArgs), 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) { // Check unruned items first, then runed versions - if (equip_type == EQUIP_SPEAR) return get_personal_count(ITEM_SPEARS) > 0 || has_any_runed_version(equip_type); - if (equip_type == EQUIP_AXE) return get_personal_count(ITEM_AXES) > 0 || has_any_runed_version(equip_type); - if (equip_type == EQUIP_SLING) return get_personal_count(ITEM_SLINGS) > 0 || has_any_runed_version(equip_type); - if (equip_type == EQUIP_BOW) return get_personal_count(ITEM_BOWS) > 0 || has_any_runed_version(equip_type); - if (equip_type == EQUIP_HAT) return get_personal_count(ITEM_SKIN_HATS) > 0 || has_any_runed_version(equip_type); - if (equip_type == EQUIP_GLOVES) return get_personal_count(ITEM_SKIN_GLOVES) > 0 || has_any_runed_version(equip_type); - if (equip_type == EQUIP_PANTS) return get_personal_count(ITEM_SKIN_PANTS) > 0 || has_any_runed_version(equip_type); - if (equip_type == EQUIP_TUNIC) return get_personal_count(ITEM_SKIN_TUNICS) > 0 || has_any_runed_version(equip_type); - if (equip_type == EQUIP_MOCCASINS) return get_personal_count(ITEM_MOCCASINS) > 0 || has_any_runed_version(equip_type); - if (equip_type == EQUIP_POUCH) return get_personal_count(ITEM_SKIN_POUCHES) > 0 || has_any_runed_version(equip_type); - if (equip_type == EQUIP_BACKPACK) return get_personal_count(ITEM_BACKPACKS) > 0 || has_any_runed_version(equip_type); - if (equip_type == EQUIP_FISHING_POLE) return get_personal_count(ITEM_FISHING_POLES) > 0; + if (equip_type == EQUIP_SPEAR) + return get_personal_count(ITEM_SPEARS) > 0 || has_any_runed_version(equip_type); + if (equip_type == EQUIP_AXE) + return get_personal_count(ITEM_AXES) > 0 || has_any_runed_version(equip_type); + if (equip_type == EQUIP_SLING) + return get_personal_count(ITEM_SLINGS) > 0 || has_any_runed_version(equip_type); + if (equip_type == EQUIP_BOW) + return get_personal_count(ITEM_BOWS) > 0 || has_any_runed_version(equip_type); + if (equip_type == EQUIP_HAT) + return get_personal_count(ITEM_SKIN_HATS) > 0 || has_any_runed_version(equip_type); + if (equip_type == EQUIP_GLOVES) + return get_personal_count(ITEM_SKIN_GLOVES) > 0 || has_any_runed_version(equip_type); + if (equip_type == EQUIP_PANTS) + return get_personal_count(ITEM_SKIN_PANTS) > 0 || has_any_runed_version(equip_type); + if (equip_type == EQUIP_TUNIC) + return get_personal_count(ITEM_SKIN_TUNICS) > 0 || has_any_runed_version(equip_type); + if (equip_type == EQUIP_MOCCASINS) + return get_personal_count(ITEM_MOCCASINS) > 0 || has_any_runed_version(equip_type); + if (equip_type == EQUIP_POUCH) + return get_personal_count(ITEM_SKIN_POUCHES) > 0 || has_any_runed_version(equip_type); + if (equip_type == EQUIP_BACKPACK) + return get_personal_count(ITEM_BACKPACKS) > 0 || has_any_runed_version(equip_type); + if (equip_type == EQUIP_FISHING_POLE) + return get_personal_count(ITEM_FISHING_POLES) > 0; return false; } void equip_equipment_type(int equip_type) { - if (equip_type == EQUIP_SPEAR || equip_type == EQUIP_AXE || equip_type == EQUIP_SLING || equip_type == EQUIP_BOW || equip_type == EQUIP_FISHING_POLE) { + if (equip_type == EQUIP_SPEAR || equip_type == EQUIP_AXE || equip_type == EQUIP_SLING || equip_type == EQUIP_BOW || + equip_type == EQUIP_FISHING_POLE) { spear_equipped = (equip_type == EQUIP_SPEAR); axe_equipped = (equip_type == EQUIP_AXE); sling_equipped = (equip_type == EQUIP_SLING); @@ -139,28 +146,47 @@ void equip_equipment_type(int equip_type) { return; } - if (equip_type == EQUIP_HAT) equipped_head = EQUIP_HAT; - else if (equip_type == EQUIP_TUNIC) equipped_torso = EQUIP_TUNIC; - else if (equip_type == EQUIP_GLOVES) equipped_hands = EQUIP_GLOVES; - else if (equip_type == EQUIP_PANTS) equipped_legs = EQUIP_PANTS; - else if (equip_type == EQUIP_MOCCASINS) equipped_feet = EQUIP_MOCCASINS; - else if (equip_type == EQUIP_POUCH) equipped_arms = EQUIP_POUCH; - else if (equip_type == EQUIP_BACKPACK) equipped_arms = EQUIP_BACKPACK; + if (equip_type == EQUIP_HAT) + equipped_head = EQUIP_HAT; + else if (equip_type == EQUIP_TUNIC) + equipped_torso = EQUIP_TUNIC; + else if (equip_type == EQUIP_GLOVES) + equipped_hands = EQUIP_GLOVES; + else if (equip_type == EQUIP_PANTS) + equipped_legs = EQUIP_PANTS; + else if (equip_type == EQUIP_MOCCASINS) + equipped_feet = EQUIP_MOCCASINS; + else if (equip_type == EQUIP_POUCH) + equipped_arms = EQUIP_POUCH; + else if (equip_type == EQUIP_BACKPACK) + equipped_arms = EQUIP_BACKPACK; } bool equipment_is_equipped(int equip_type) { - if (equip_type == EQUIP_SPEAR) return spear_equipped; - if (equip_type == EQUIP_AXE) return axe_equipped; - if (equip_type == EQUIP_SLING) return sling_equipped; - if (equip_type == EQUIP_BOW) return bow_equipped; - if (equip_type == EQUIP_FISHING_POLE) return fishing_pole_equipped; - if (equip_type == EQUIP_HAT) return equipped_head == EQUIP_HAT; - if (equip_type == EQUIP_TUNIC) return equipped_torso == EQUIP_TUNIC; - if (equip_type == EQUIP_GLOVES) return equipped_hands == EQUIP_GLOVES; - if (equip_type == EQUIP_PANTS) return equipped_legs == EQUIP_PANTS; - if (equip_type == EQUIP_MOCCASINS) return equipped_feet == EQUIP_MOCCASINS; - if (equip_type == EQUIP_POUCH) return equipped_arms == EQUIP_POUCH; - if (equip_type == EQUIP_BACKPACK) return equipped_arms == EQUIP_BACKPACK; + if (equip_type == EQUIP_SPEAR) + return spear_equipped; + if (equip_type == EQUIP_AXE) + return axe_equipped; + if (equip_type == EQUIP_SLING) + return sling_equipped; + if (equip_type == EQUIP_BOW) + return bow_equipped; + if (equip_type == EQUIP_FISHING_POLE) + return fishing_pole_equipped; + if (equip_type == EQUIP_HAT) + return equipped_head == EQUIP_HAT; + if (equip_type == EQUIP_TUNIC) + return equipped_torso == EQUIP_TUNIC; + if (equip_type == EQUIP_GLOVES) + return equipped_hands == EQUIP_GLOVES; + if (equip_type == EQUIP_PANTS) + return equipped_legs == EQUIP_PANTS; + if (equip_type == EQUIP_MOCCASINS) + return equipped_feet == EQUIP_MOCCASINS; + if (equip_type == EQUIP_POUCH) + return equipped_arms == EQUIP_POUCH; + if (equip_type == EQUIP_BACKPACK) + return equipped_arms == EQUIP_BACKPACK; return false; } @@ -194,11 +220,16 @@ void unequip_equipment_type(int equip_type) { void update_max_health_from_equipment() { int bonus = 0; - if (equipped_head == EQUIP_HAT) bonus += HAT_MAX_HEALTH_BONUS; - if (equipped_hands == EQUIP_GLOVES) bonus += GLOVES_MAX_HEALTH_BONUS; - if (equipped_legs == EQUIP_PANTS) bonus += PANTS_MAX_HEALTH_BONUS; - if (equipped_torso == EQUIP_TUNIC) bonus += TUNIC_MAX_HEALTH_BONUS; - if (equipped_feet == EQUIP_MOCCASINS) bonus += MOCCASINS_MAX_HEALTH_BONUS; + if (equipped_head == EQUIP_HAT) + bonus += HAT_MAX_HEALTH_BONUS; + if (equipped_hands == EQUIP_GLOVES) + bonus += GLOVES_MAX_HEALTH_BONUS; + if (equipped_legs == EQUIP_PANTS) + bonus += PANTS_MAX_HEALTH_BONUS; + if (equipped_torso == EQUIP_TUNIC) + bonus += TUNIC_MAX_HEALTH_BONUS; + if (equipped_feet == EQUIP_MOCCASINS) + bonus += MOCCASINS_MAX_HEALTH_BONUS; max_health = base_max_health + bonus; if (player_health > max_health) { player_health = max_health; @@ -214,25 +245,37 @@ void update_max_health_from_equipment() { // Apply blessing bonus on top of existing speed if (blessing_speed_active) { int blessing_bonus = BASE_WALK_SPEED - BLESSING_WALK_SPEED; - if (blessing_bonus < 0) blessing_bonus = 0; + if (blessing_bonus < 0) + blessing_bonus = 0; walk_speed -= blessing_bonus; } // Ensure minimum walk speed - if (walk_speed < 200) walk_speed = 200; + if (walk_speed < 200) + walk_speed = 200; } int get_quick_slot_key() { - if (key_pressed(KEY_1)) return 1; - if (key_pressed(KEY_2)) return 2; - if (key_pressed(KEY_3)) return 3; - if (key_pressed(KEY_4)) return 4; - if (key_pressed(KEY_5)) return 5; - if (key_pressed(KEY_6)) return 6; - if (key_pressed(KEY_7)) return 7; - if (key_pressed(KEY_8)) return 8; - if (key_pressed(KEY_9)) return 9; - if (key_pressed(KEY_0)) return 0; + if (key_pressed(KEY_1)) + return 1; + if (key_pressed(KEY_2)) + return 2; + if (key_pressed(KEY_3)) + return 3; + if (key_pressed(KEY_4)) + return 4; + if (key_pressed(KEY_5)) + return 5; + if (key_pressed(KEY_6)) + return 6; + if (key_pressed(KEY_7)) + return 7; + if (key_pressed(KEY_8)) + return 8; + if (key_pressed(KEY_9)) + return 9; + if (key_pressed(KEY_0)) + return 0; return -1; } @@ -279,26 +322,46 @@ string get_equipment_display_name_with_rune(int equip_type, int rune_type) { } bool is_breakable_item_type(int itemType) { - if (is_runed_item_type(itemType)) return true; - if (itemType == ITEM_SPEARS) return true; - if (itemType == ITEM_SLINGS) return true; - if (itemType == ITEM_AXES) return true; - if (itemType == ITEM_SNARES) return true; - if (itemType == ITEM_KNIVES) return true; - if (itemType == ITEM_FISHING_POLES) return true; - if (itemType == ITEM_SKIN_HATS) return true; - if (itemType == ITEM_SKIN_GLOVES) return true; - if (itemType == ITEM_SKIN_PANTS) return true; - if (itemType == ITEM_SKIN_TUNICS) return true; - if (itemType == ITEM_MOCCASINS) return true; - if (itemType == ITEM_SKIN_POUCHES) return true; - if (itemType == ITEM_ROPES) return true; - if (itemType == ITEM_REED_BASKETS) return true; - if (itemType == ITEM_CLAY_POTS) return true; - if (itemType == ITEM_BOWS) return true; - if (itemType == ITEM_QUIVERS) return true; - if (itemType == ITEM_BACKPACKS) return true; - if (itemType == ITEM_CANOES) return true; + if (is_runed_item_type(itemType)) + return true; + if (itemType == ITEM_SPEARS) + return true; + if (itemType == ITEM_SLINGS) + return true; + if (itemType == ITEM_AXES) + return true; + if (itemType == ITEM_SNARES) + return true; + if (itemType == ITEM_KNIVES) + return true; + if (itemType == ITEM_FISHING_POLES) + return true; + if (itemType == ITEM_SKIN_HATS) + return true; + if (itemType == ITEM_SKIN_GLOVES) + return true; + if (itemType == ITEM_SKIN_PANTS) + return true; + if (itemType == ITEM_SKIN_TUNICS) + return true; + if (itemType == ITEM_MOCCASINS) + return true; + if (itemType == ITEM_SKIN_POUCHES) + return true; + if (itemType == ITEM_ROPES) + return true; + if (itemType == ITEM_REED_BASKETS) + return true; + if (itemType == ITEM_CLAY_POTS) + return true; + if (itemType == ITEM_BOWS) + return true; + if (itemType == ITEM_QUIVERS) + return true; + if (itemType == ITEM_BACKPACKS) + return true; + if (itemType == ITEM_CANOES) + return true; return false; } @@ -312,12 +375,15 @@ string get_breakable_item_name(int itemType) { return get_item_label_singular(itemType); } -void get_breakable_personal_item_types(int[]@ items) { - if (@items == null) return; +void get_breakable_personal_item_types(int[] @items) { + if (@items == null) + return; items.resize(0); for (int itemType = 0; itemType < ITEM_COUNT; itemType++) { - if (!is_breakable_item_type(itemType)) continue; - if (get_personal_count(itemType) <= 0) continue; + if (!is_breakable_item_type(itemType)) + continue; + if (get_personal_count(itemType) <= 0) + continue; items.insert_last(itemType); } @@ -329,7 +395,8 @@ void get_breakable_personal_item_types(int[]@ items) { for (uint j = 0; j < runeTypes.length(); j++) { int runeType = runeTypes[j]; int runedCount = get_runed_item_count(equipType, runeType); - if (runedCount <= 0) continue; + if (runedCount <= 0) + continue; int encoded = encode_runed_item_type(equipType, runeType); items.insert_last(encoded); } @@ -342,7 +409,8 @@ bool remove_breakable_personal_item(int itemType) { int runeType = 0; decode_runed_item_type(itemType, equipType, runeType); int current = get_runed_item_count(equipType, runeType); - if (current <= 0) return false; + if (current <= 0) + return false; remove_runed_item(equipType, runeType); if (get_runed_item_count(equipType, runeType) <= 0 && get_equipped_rune_for_slot(equipType) == runeType) { clear_equipped_rune_for_slot(equipType); @@ -350,19 +418,24 @@ bool remove_breakable_personal_item(int itemType) { return true; } - if (!is_breakable_item_type(itemType)) return false; - if (get_personal_count(itemType) <= 0) return false; + if (!is_breakable_item_type(itemType)) + return false; + if (get_personal_count(itemType) <= 0) + return false; add_personal_count(itemType, -1); return true; } bool try_consume_heal_scroll() { - if (player_health > 0) return false; - if (get_personal_count(ITEM_HEAL_SCROLL) <= 0) return false; + if (player_health > 0) + return false; + if (get_personal_count(ITEM_HEAL_SCROLL) <= 0) + return false; add_personal_count(ITEM_HEAL_SCROLL, -1); player_health = max_health / 2; - if (player_health < 1) player_health = 1; + if (player_health < 1) + player_health = 1; p.play_stationary("sounds/actions/heal_scroll.ogg", false); return true; } @@ -386,7 +459,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; } @@ -394,35 +469,41 @@ void activate_quick_slot(int slot_index) { if (equipment_is_equipped(equip_type)) { if (get_equipped_rune_for_slot(equip_type) != rune_type) { if (rune_type != RUNE_NONE && get_runed_item_count(equip_type, rune_type) <= 0) { - speak_with_history("Item not available.", true); + speak_with_history(tr("system.inventory.item_not_available"), true); return; } set_equipped_rune_for_slot(equip_type, rune_type); update_max_health_from_equipment(); - speak_with_history(get_equipment_display_name_with_rune(equip_type, rune_type) + " equipped.", true); + dictionary equip_args; + equip_args.set("name", get_equipment_display_name_with_rune(equip_type, rune_type)); + speak_with_history(trf("system.equipment.equipped", equip_args), true); return; } unequip_equipment_type(equip_type); clear_equipped_rune_for_slot(equip_type); update_max_health_from_equipment(); - speak_with_history(get_equipment_display_name_with_rune(equip_type, rune_type) + " unequipped.", true); + dictionary unequip_args; + unequip_args.set("name", get_equipment_display_name_with_rune(equip_type, rune_type)); + speak_with_history(trf("system.equipment.unequipped", unequip_args), true); return; } if (rune_type != RUNE_NONE) { if (get_runed_item_count(equip_type, rune_type) <= 0) { - speak_with_history("Item not available.", true); + speak_with_history(tr("system.inventory.item_not_available"), true); return; } } else if (!equipment_available(equip_type)) { - speak_with_history("Item not available.", true); + speak_with_history(tr("system.inventory.item_not_available"), true); return; } equip_equipment_type(equip_type); set_equipped_rune_for_slot(equip_type, rune_type); update_max_health_from_equipment(); - speak_with_history(get_equipment_display_name_with_rune(equip_type, rune_type) + " equipped.", true); + dictionary equip_args; + equip_args.set("name", get_equipment_display_name_with_rune(equip_type, rune_type)); + speak_with_history(trf("system.equipment.equipped", equip_args), true); } void check_quick_slot_keys() { @@ -433,61 +514,81 @@ void check_quick_slot_keys() { } int add_to_stack(int current, int amount) { - if (amount <= 0) return 0; + if (amount <= 0) + return 0; int space = get_personal_stack_limit() - current; - if (space <= 0) return 0; - if (amount > space) return space; + if (space <= 0) + return 0; + if (amount > space) + return space; return amount; } string format_favor(double value) { - if (value < 0) value = 0; + if (value < 0) + value = 0; int scaled = int((value * 100.0) + 0.5); int wholePart = scaled / 100; int fractionalPart = scaled % 100; - if (fractionalPart <= 0) return "" + wholePart; - if (fractionalPart < 10) return wholePart + ".0" + fractionalPart; - if (fractionalPart % 10 == 0) return wholePart + "." + (fractionalPart / 10); + if (fractionalPart <= 0) + return "" + wholePart; + if (fractionalPart < 10) + return wholePart + ".0" + fractionalPart; + if (fractionalPart % 10 == 0) + return wholePart + "." + (fractionalPart / 10); return wholePart + "." + fractionalPart; } string get_equipped_weapon_name() { - if (spear_equipped) return "Spear"; - if (axe_equipped) return "Stone Axe"; - if (sling_equipped) return "Sling"; - if (bow_equipped) return "Bow"; - if (fishing_pole_equipped) return "Fishing Pole"; - return "None"; + if (spear_equipped) + return get_base_equipment_name(EQUIP_SPEAR); + if (axe_equipped) + return get_base_equipment_name(EQUIP_AXE); + if (sling_equipped) + return get_base_equipment_name(EQUIP_SLING); + if (bow_equipped) + return get_base_equipment_name(EQUIP_BOW); + if (fishing_pole_equipped) + 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() { - if (!equipment_available(EQUIP_SPEAR)) spear_equipped = false; - if (!equipment_available(EQUIP_AXE)) axe_equipped = false; - if (!equipment_available(EQUIP_SLING)) sling_equipped = false; - if (!equipment_available(EQUIP_BOW)) bow_equipped = false; - if (!equipment_available(EQUIP_FISHING_POLE)) fishing_pole_equipped = false; + if (!equipment_available(EQUIP_SPEAR)) + spear_equipped = false; + if (!equipment_available(EQUIP_AXE)) + axe_equipped = false; + if (!equipment_available(EQUIP_SLING)) + sling_equipped = false; + if (!equipment_available(EQUIP_BOW)) + bow_equipped = false; + if (!equipment_available(EQUIP_FISHING_POLE)) + fishing_pole_equipped = false; - bool any_weapon_equipped = spear_equipped || axe_equipped || sling_equipped || bow_equipped || fishing_pole_equipped; - if (!any_weapon_equipped) equipped_weapon_rune = RUNE_NONE; + bool any_weapon_equipped = + spear_equipped || axe_equipped || sling_equipped || bow_equipped || fishing_pole_equipped; + if (!any_weapon_equipped) + equipped_weapon_rune = RUNE_NONE; if (!equipment_available(EQUIP_HAT)) { equipped_head = EQUIP_NONE; 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 3caca8d..33b43d7 100644 --- a/src/item_registry.nvgt +++ b/src/item_registry.nvgt @@ -103,15 +103,20 @@ void init_item_registry() { item_registry[ITEM_AXES] = ItemDefinition(ITEM_AXES, "axes", "axe", "Axes", 1.50); item_registry[ITEM_SNARES] = ItemDefinition(ITEM_SNARES, "snares", "snare", "Snares", 0.50); item_registry[ITEM_KNIVES] = ItemDefinition(ITEM_KNIVES, "knives", "knife", "Knives", 0.80); - item_registry[ITEM_FISHING_POLES] = ItemDefinition(ITEM_FISHING_POLES, "fishing poles", "fishing pole", "Fishing Poles", 0.80); + item_registry[ITEM_FISHING_POLES] = + ItemDefinition(ITEM_FISHING_POLES, "fishing poles", "fishing pole", "Fishing Poles", 0.80); item_registry[ITEM_SKIN_HATS] = ItemDefinition(ITEM_SKIN_HATS, "skin hats", "skin hat", "Skin Hats", 0.60); - item_registry[ITEM_SKIN_GLOVES] = ItemDefinition(ITEM_SKIN_GLOVES, "skin gloves", "skin glove", "Skin Gloves", 0.60); + item_registry[ITEM_SKIN_GLOVES] = + ItemDefinition(ITEM_SKIN_GLOVES, "skin gloves", "skin glove", "Skin Gloves", 0.60); item_registry[ITEM_SKIN_PANTS] = ItemDefinition(ITEM_SKIN_PANTS, "skin pants", "skin pants", "Skin Pants", 1.20); - item_registry[ITEM_SKIN_TUNICS] = ItemDefinition(ITEM_SKIN_TUNICS, "skin tunics", "skin tunic", "Skin Tunics", 1.20); + item_registry[ITEM_SKIN_TUNICS] = + ItemDefinition(ITEM_SKIN_TUNICS, "skin tunics", "skin tunic", "Skin Tunics", 1.20); item_registry[ITEM_MOCCASINS] = ItemDefinition(ITEM_MOCCASINS, "moccasins", "moccasin", "Moccasins", 0.80); - item_registry[ITEM_SKIN_POUCHES] = ItemDefinition(ITEM_SKIN_POUCHES, "skin pouches", "skin pouch", "Skin Pouches", 0.80); + item_registry[ITEM_SKIN_POUCHES] = + ItemDefinition(ITEM_SKIN_POUCHES, "skin pouches", "skin pouch", "Skin Pouches", 0.80); item_registry[ITEM_ROPES] = ItemDefinition(ITEM_ROPES, "ropes", "rope", "Ropes", 0.40); - item_registry[ITEM_REED_BASKETS] = ItemDefinition(ITEM_REED_BASKETS, "reed baskets", "reed basket", "Reed Baskets", 0.60); + item_registry[ITEM_REED_BASKETS] = + ItemDefinition(ITEM_REED_BASKETS, "reed baskets", "reed basket", "Reed Baskets", 0.60); item_registry[ITEM_CLAY_POTS] = ItemDefinition(ITEM_CLAY_POTS, "clay pots", "clay pot", "Clay Pots", 0.70); item_registry[ITEM_FEATHERS] = ItemDefinition(ITEM_FEATHERS, "feathers", "feather", "Feathers", 0.05); item_registry[ITEM_DOWN] = ItemDefinition(ITEM_DOWN, "down", "down", "Down", 0.05); @@ -121,64 +126,36 @@ void init_item_registry() { item_registry[ITEM_QUIVERS] = ItemDefinition(ITEM_QUIVERS, "quivers", "quiver", "Quivers", 1.50); item_registry[ITEM_BOWSTRINGS] = ItemDefinition(ITEM_BOWSTRINGS, "bowstrings", "bowstring", "Bowstrings", 0.20); item_registry[ITEM_SINEW] = ItemDefinition(ITEM_SINEW, "sinew", "piece of sinew", "Sinew", 0.10); - item_registry[ITEM_BOAR_CARCASSES] = ItemDefinition(ITEM_BOAR_CARCASSES, "boar carcasses", "boar carcass", "Boar Carcasses", 1.50); + item_registry[ITEM_BOAR_CARCASSES] = + ItemDefinition(ITEM_BOAR_CARCASSES, "boar carcasses", "boar carcass", "Boar Carcasses", 1.50); item_registry[ITEM_BACKPACKS] = ItemDefinition(ITEM_BACKPACKS, "backpacks", "backpack", "Backpacks", 2.50); item_registry[ITEM_CANOES] = ItemDefinition(ITEM_CANOES, "canoes", "canoe", "Canoes", 4.00); item_registry[ITEM_FISH] = ItemDefinition(ITEM_FISH, "fish", "fish", "Fish", 0.10); - item_registry[ITEM_SMOKED_FISH] = ItemDefinition(ITEM_SMOKED_FISH, "smoked fish", "smoked fish", "Smoked Fish", 0.20); - item_registry[ITEM_HEAL_SCROLL] = ItemDefinition(ITEM_HEAL_SCROLL, "heal scrolls", "heal scroll", "Heal Scrolls", 0.50); - item_registry[ITEM_BASKET_FOOD] = ItemDefinition(ITEM_BASKET_FOOD, "baskets of fruits and nuts", "basket of fruits and nuts", "Baskets of Fruits and Nuts", 0.15); + item_registry[ITEM_SMOKED_FISH] = + ItemDefinition(ITEM_SMOKED_FISH, "smoked fish", "smoked fish", "Smoked Fish", 0.20); + item_registry[ITEM_HEAL_SCROLL] = + ItemDefinition(ITEM_HEAL_SCROLL, "heal scrolls", "heal scroll", "Heal Scrolls", 0.50); + item_registry[ITEM_BASKET_FOOD] = ItemDefinition(ITEM_BASKET_FOOD, "baskets of fruits and nuts", + "basket of fruits and nuts", "Baskets of Fruits and Nuts", 0.15); // Define display order for inventory menus // This controls the order items appear in menus inventory_display_order = { // Raw materials - ITEM_STICKS, - ITEM_VINES, - ITEM_REEDS, - ITEM_STONES, - ITEM_LOGS, - ITEM_CLAY, + ITEM_STICKS, ITEM_VINES, ITEM_REEDS, ITEM_STONES, ITEM_LOGS, ITEM_CLAY, // Hunting drops - ITEM_SMALL_GAME, - ITEM_BOAR_CARCASSES, - ITEM_MEAT, - ITEM_SKINS, - ITEM_FEATHERS, - ITEM_DOWN, - ITEM_SINEW, + ITEM_SMALL_GAME, ITEM_BOAR_CARCASSES, ITEM_MEAT, ITEM_SKINS, ITEM_FEATHERS, ITEM_DOWN, ITEM_SINEW, // Food items - ITEM_FISH, - ITEM_SMOKED_FISH, - ITEM_BASKET_FOOD, + ITEM_FISH, ITEM_SMOKED_FISH, ITEM_BASKET_FOOD, // Misc items - ITEM_INCENSE, - ITEM_HEAL_SCROLL, + ITEM_INCENSE, ITEM_HEAL_SCROLL, // Weapons - ITEM_SPEARS, - ITEM_SLINGS, - ITEM_AXES, - ITEM_BOWS, - ITEM_ARROWS, - ITEM_QUIVERS, - ITEM_BOWSTRINGS, + ITEM_SPEARS, ITEM_SLINGS, ITEM_AXES, ITEM_BOWS, ITEM_ARROWS, ITEM_QUIVERS, ITEM_BOWSTRINGS, // Tools - ITEM_SNARES, - ITEM_KNIVES, - ITEM_FISHING_POLES, - ITEM_ROPES, - ITEM_REED_BASKETS, - ITEM_CLAY_POTS, - ITEM_CANOES, + ITEM_SNARES, ITEM_KNIVES, ITEM_FISHING_POLES, ITEM_ROPES, ITEM_REED_BASKETS, ITEM_CLAY_POTS, ITEM_CANOES, // Clothing - ITEM_SKIN_HATS, - ITEM_SKIN_GLOVES, - ITEM_SKIN_PANTS, - ITEM_SKIN_TUNICS, - ITEM_MOCCASINS, - ITEM_SKIN_POUCHES, - ITEM_BACKPACKS - }; + ITEM_SKIN_HATS, ITEM_SKIN_GLOVES, ITEM_SKIN_PANTS, ITEM_SKIN_TUNICS, ITEM_MOCCASINS, ITEM_SKIN_POUCHES, + ITEM_BACKPACKS}; // Initialize inventory arrays personal_inventory.resize(ITEM_COUNT); @@ -202,36 +179,44 @@ void reset_inventory() { // Accessor functions for personal inventory int get_personal_count(int item_type) { - if (item_type < 0 || item_type >= ITEM_COUNT) return 0; + if (item_type < 0 || item_type >= ITEM_COUNT) + return 0; return personal_inventory[item_type]; } void set_personal_count(int item_type, int count) { - if (item_type < 0 || item_type >= ITEM_COUNT) return; + if (item_type < 0 || item_type >= ITEM_COUNT) + return; personal_inventory[item_type] = count; } void add_personal_count(int item_type, int amount) { - if (item_type < 0 || item_type >= ITEM_COUNT) return; + if (item_type < 0 || item_type >= ITEM_COUNT) + return; personal_inventory[item_type] += amount; - if (personal_inventory[item_type] < 0) personal_inventory[item_type] = 0; + if (personal_inventory[item_type] < 0) + personal_inventory[item_type] = 0; } // Accessor functions for storage inventory int get_storage_count(int item_type) { - if (item_type < 0 || item_type >= ITEM_COUNT) return 0; + if (item_type < 0 || item_type >= ITEM_COUNT) + return 0; return storage_inventory[item_type]; } void set_storage_count(int item_type, int count) { - if (item_type < 0 || item_type >= ITEM_COUNT) return; + if (item_type < 0 || item_type >= ITEM_COUNT) + return; storage_inventory[item_type] = count; } void add_storage_count(int item_type, int amount) { - if (item_type < 0 || item_type >= ITEM_COUNT) return; + if (item_type < 0 || item_type >= ITEM_COUNT) + return; storage_inventory[item_type] += amount; - if (storage_inventory[item_type] < 0) storage_inventory[item_type] = 0; + if (storage_inventory[item_type] < 0) + storage_inventory[item_type] = 0; } int get_default_fish_weight() { @@ -239,8 +224,10 @@ int get_default_fish_weight() { } int clamp_fish_weight(int weight) { - if (weight < FISH_WEIGHT_MIN) return FISH_WEIGHT_MIN; - if (weight > FISH_WEIGHT_MAX) return FISH_WEIGHT_MAX; + if (weight < FISH_WEIGHT_MIN) + return FISH_WEIGHT_MIN; + if (weight > FISH_WEIGHT_MAX) + return FISH_WEIGHT_MAX; return weight; } @@ -253,14 +240,16 @@ void add_storage_fish_weight(int weight) { } int pop_personal_fish_weight() { - if (personal_fish_weights.length() == 0) return get_default_fish_weight(); + if (personal_fish_weights.length() == 0) + return get_default_fish_weight(); int weight = personal_fish_weights[0]; personal_fish_weights.remove_at(0); return clamp_fish_weight(weight); } int pop_storage_fish_weight() { - if (storage_fish_weights.length() == 0) return get_default_fish_weight(); + if (storage_fish_weights.length() == 0) + return get_default_fish_weight(); int weight = storage_fish_weights[0]; storage_fish_weights.remove_at(0); return clamp_fish_weight(weight); @@ -280,30 +269,37 @@ void move_fish_weights_to_personal(int amount) { int get_smoked_fish_yield(int weight) { int clamped = clamp_fish_weight(weight); - if (clamped <= 7) return 1; - if (clamped <= 17) return 2; - if (clamped <= 27) return 3; + if (clamped <= 7) + return 1; + if (clamped <= 17) + return 2; + if (clamped <= 27) + return 3; return 4; } // 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; + if (item_type < 0 || item_type >= ITEM_COUNT) + 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; + if (item_type < 0 || item_type >= ITEM_COUNT) + 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; + if (item_type < 0 || item_type >= ITEM_COUNT) + 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) { - if (item_type < 0 || item_type >= ITEM_COUNT) return 0.01; + if (item_type < 0 || item_type >= ITEM_COUNT) + return 0.01; return item_registry[item_type].favor_value; } @@ -315,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/learn_sounds.nvgt b/src/learn_sounds.nvgt index e3fbb9a..35358a8 100644 --- a/src/learn_sounds.nvgt +++ b/src/learn_sounds.nvgt @@ -1,11 +1,11 @@ #include "libstorm-nvgt/learn_sounds.nvgt" #include "libstorm-nvgt/docs_browser.nvgt" -#include "excluded_sounds.nvgt" +#include "src/sound_settings.nvgt" bool learnSoundsConfigured = false; bool docsBrowserConfigured = false; -void learn_sounds_bridge_speak(const string &in message, bool interrupt) { +void learn_sounds_bridge_speak(const string& in message, bool interrupt) { speak_with_history(message, interrupt); } @@ -14,7 +14,8 @@ void learn_sounds_bridge_tick() { } void configure_docs_browser_if_needed() { - if (docsBrowserConfigured) return; + if (docsBrowserConfigured) + return; docs_browser_set_speak_callback(learn_sounds_bridge_speak); docs_browser_set_tick_callback(learn_sounds_bridge_tick); @@ -26,13 +27,14 @@ void configure_docs_browser_if_needed() { docsBrowserConfigured = true; } -void add_doc_entries(string[]@ labels, string[]@ paths, int[]@ types) { +void add_doc_entries(string[] @labels, string[] @paths, int[] @types) { configure_docs_browser_if_needed(); docs_browser_add_entries(labels, paths, types, 1); } void configure_learn_sounds_if_needed() { - if (learnSoundsConfigured) return; + if (learnSoundsConfigured) + return; learn_sounds_set_speak_callback(learn_sounds_bridge_speak); learn_sounds_set_tick_callback(learn_sounds_bridge_tick); diff --git a/src/menus/action_menu.nvgt b/src/menus/action_menu.nvgt index ea0a7ac..1ed2524 100644 --- a/src/menus/action_menu.nvgt +++ b/src/menus/action_menu.nvgt @@ -7,8 +7,8 @@ void check_action_menu(int x) { } } -WorldSnare@ get_snare_within_range(int pos, int range) { - WorldSnare@ nearest = null; +WorldSnare @get_snare_within_range(int pos, int range) { + WorldSnare @nearest = null; int nearest_distance = range + 1; for (uint i = 0; i < world_snares.length(); i++) { int dist = abs(world_snares[i].position - pos); @@ -22,110 +22,121 @@ WorldSnare@ get_snare_within_range(int pos, int range) { string get_fire_intensity_label(int fuel_remaining) { int hours_remaining = fuel_remaining / 60000; - if (hours_remaining >= 8) return "Giant blaze"; - if (hours_remaining >= 2) return "Small fire"; - return "A few glowing coals"; + if (hours_remaining >= 8) + return tr("system.action.fire.intensity.giant_blaze"); + if (hours_remaining >= 2) + return tr("system.action.fire.intensity.small_fire"); + return tr("system.action.fire.intensity.glowing_coals"); } string get_fire_time_estimate(int fuel_remaining) { int approx_hours = (fuel_remaining + 30000) / 60000; - if (approx_hours <= 0) return "less than 1 hour"; - if (approx_hours == 1) return "approximately 1 hour"; - return "approximately " + approx_hours + " hours"; + if (approx_hours <= 0) + return tr("system.action.fire.estimate.less_than_one_hour"); + if (approx_hours == 1) + return tr("system.action.fire.estimate.approximately_one_hour"); + dictionary args; + args.set("hours", approx_hours); + return trf("system.action.fire.estimate.approximately_hours", args); } -void check_fire_status(WorldFire@ fire) { +void check_fire_status(WorldFire @fire) { if (fire == null) { - speak_with_history("No fire nearby.", true); + speak_with_history(tr("system.action.no_fire_nearby"), true); return; } string label = get_fire_intensity_label(fire.fuel_remaining); string estimate = get_fire_time_estimate(fire.fuel_remaining); - speak_with_history(label + ". " + estimate + " remaining.", true); + dictionary args; + args.set("label", label); + args.set("estimate", estimate); + speak_with_history(trf("system.action.fire.status", args), true); } -void check_snare_status(WorldSnare@ snare) { +void check_snare_status(WorldSnare @snare) { if (snare == null) { - speak_with_history("No snare nearby.", true); + speak_with_history(tr("system.action.no_snare_nearby"), true); return; } if (snare.has_catch) { - speak_with_history("Snare holds a " + snare.catch_type + ".", true); + dictionary args; + args.set("catch_type", i18n_translate_speech_message(snare.catch_type)); + speak_with_history(trf("system.action.snare_holds", args), true); } else if (!snare.active) { - speak_with_history("Snare is set but not active yet.", true); + speak_with_history(tr("system.action.snare_set_inactive"), true); } else { - speak_with_history("Snare is set and empty.", true); + speak_with_history(tr("system.action.snare_set_empty"), true); } } void check_fishing_status() { if (fish_on_line) { - speak_with_history("Fish on the line.", true); + speak_with_history(tr("system.action.fish_on_line"), true); return; } - speak_with_history("No fish on the line.", true); + speak_with_history(tr("system.action.no_fish_on_line"), true); } void try_place_snare(int x) { if (get_personal_count(ITEM_SNARES) > 0) { // Prevent placing in base area if (x <= BASE_END) { - speak_with_history("Cannot place snares in the base area.", true); + speak_with_history(tr("system.action.cannot_place_snares_in_base"), true); return; } // Prevent placing if one already exists here if (get_snare_at(x) != null) { - speak_with_history("There is already a snare here.", true); + speak_with_history(tr("system.action.snare_already_here"), true); return; } add_personal_count(ITEM_SNARES, -1); add_world_snare(x); - speak_with_history("Snare set.", true); + speak_with_history(tr("system.action.snare_set"), true); } else { - speak_with_history("No snares to place.", true); + speak_with_history(tr("system.action.no_snares_to_place"), true); } } -void try_feed_fire_stick(WorldFire@ fire) { +void try_feed_fire_stick(WorldFire @fire) { if (get_personal_count(ITEM_STICKS) > 0 && fire != null) { add_personal_count(ITEM_STICKS, -1); fire.add_fuel(300000); // 5 minutes - speak_with_history("You dump an arm load of sticks into the fire.", true); + speak_with_history(tr("system.action.feed_fire.stick"), true); p.play_stationary("sounds/actions/feed_fire.ogg", false); } } -void try_feed_fire_vine(WorldFire@ fire) { +void try_feed_fire_vine(WorldFire @fire) { if (get_personal_count(ITEM_VINES) > 0 && fire != null) { add_personal_count(ITEM_VINES, -1); fire.add_fuel(60000); // 1 minute - speak_with_history("You toss a fiew vines and leaves into the fire.", true); + speak_with_history(tr("system.action.feed_fire.vine"), true); p.play_stationary("sounds/actions/feed_fire.ogg", false); } } -void try_feed_fire_log(WorldFire@ fire) { +void try_feed_fire_log(WorldFire @fire) { if (get_personal_count(ITEM_LOGS) > 0 && fire != null) { add_personal_count(ITEM_LOGS, -1); fire.add_fuel(720000); // 12 minutes - speak_with_history("You heave a log into the fire.", true); + speak_with_history(tr("system.action.feed_fire.log"), true); p.play_stationary("sounds/actions/feed_fire.ogg", false); } } void try_burn_incense() { if (world_altars.length() == 0) { - speak_with_history("No altar built.", true); + speak_with_history(tr("system.action.no_altar_built"), true); return; } if (get_personal_count(ITEM_CLAY_POTS) <= 0) { - speak_with_history("You need a clay pot to burn incense.", true); + speak_with_history(tr("system.action.need_clay_pot_for_incense"), true); return; } if (get_personal_count(ITEM_INCENSE) <= 0) { - speak_with_history("No incense to burn.", true); + speak_with_history(tr("system.action.no_incense_to_burn"), true); return; } @@ -133,12 +144,14 @@ void try_burn_incense() { add_personal_count(ITEM_CLAY_POTS, -1); incense_hours_remaining += INCENSE_HOURS_PER_STICK; incense_burning = true; - speak_with_history("Incense burning. " + incense_hours_remaining + " hours remaining.", true); + dictionary args; + args.set("hours", incense_hours_remaining); + speak_with_history(trf("system.action.incense_burning", args), true); } -void try_feed_fire_stick_max(WorldFire@ fire) { +void try_feed_fire_stick_max(WorldFire @fire) { if (get_personal_count(ITEM_STICKS) <= 0 || fire == null) { - speak_with_history("No sticks to feed fire.", true); + speak_with_history(tr("system.action.no_sticks_to_feed_fire"), true); return; } @@ -148,12 +161,14 @@ void try_feed_fire_stick_max(WorldFire@ fire) { fire.add_fuel(fuel_added); p.play_stationary("sounds/actions/feed_fire.ogg", false); - speak_with_history("Dumped " + amount + " sticks into the fire.", true); + dictionary args; + args.set("amount", amount); + speak_with_history(trf("system.action.dumped_sticks", args), true); } -void try_feed_fire_vine_max(WorldFire@ fire) { +void try_feed_fire_vine_max(WorldFire @fire) { if (get_personal_count(ITEM_VINES) <= 0 || fire == null) { - speak_with_history("No vines to feed fire.", true); + speak_with_history(tr("system.action.no_vines_to_feed_fire"), true); return; } @@ -163,12 +178,14 @@ void try_feed_fire_vine_max(WorldFire@ fire) { fire.add_fuel(fuel_added); p.play_stationary("sounds/actions/feed_fire.ogg", false); - speak_with_history("Dumped " + amount + " vines into the fire.", true); + dictionary args; + args.set("amount", amount); + speak_with_history(trf("system.action.dumped_vines", args), true); } -void try_feed_fire_log_max(WorldFire@ fire) { +void try_feed_fire_log_max(WorldFire @fire) { if (get_personal_count(ITEM_LOGS) <= 0 || fire == null) { - speak_with_history("No logs to feed fire.", true); + speak_with_history(tr("system.action.no_logs_to_feed_fire"), true); return; } @@ -178,20 +195,22 @@ void try_feed_fire_log_max(WorldFire@ fire) { fire.add_fuel(fuel_added); p.play_stationary("sounds/actions/feed_fire.ogg", false); - speak_with_history("Dumped " + amount + " logs into the fire.", true); + dictionary args; + args.set("amount", amount); + speak_with_history(trf("system.action.dumped_logs", args), true); } void try_burn_incense_max() { if (world_altars.length() == 0) { - speak_with_history("No altar built.", true); + speak_with_history(tr("system.action.no_altar_built"), true); return; } if (get_personal_count(ITEM_CLAY_POTS) <= 0) { - speak_with_history("You need a clay pot to burn incense.", true); + speak_with_history(tr("system.action.need_clay_pot_for_incense"), true); return; } if (get_personal_count(ITEM_INCENSE) <= 0) { - speak_with_history("No incense to burn.", true); + speak_with_history(tr("system.action.no_incense_to_burn"), true); return; } @@ -203,81 +222,92 @@ void try_burn_incense_max() { incense_hours_remaining += total_hours; incense_burning = true; - speak_with_history("Burned " + amount + " incense. +" + total_hours + " hours.", true); + dictionary args; + args.set("amount", amount); + args.set("hours", total_hours); + speak_with_history(trf("system.action.burned_incense", args), true); } void run_action_menu(int x) { - speak_with_history("Action menu.", true); - int selection = 0; string[] options; int[] action_types; // Track what action each option corresponds to const int check_range = 2; // Check if fire is nearby - WorldFire@ nearby_fire = get_fire_near(x); + WorldFire @nearby_fire = get_fire_near(x); bool can_feed_fire = nearby_fire != null; - WorldFire@ check_fire = get_fire_within_range(x, check_range); - WorldSnare@ check_snare = get_snare_within_range(x, check_range); + WorldFire @check_fire = get_fire_within_range(x, check_range); + WorldSnare @check_snare = get_snare_within_range(x, check_range); // Build menu options dynamically - options.insert_last("Place Snare"); + options.insert_last(tr("system.action.option.place_snare")); action_types.insert_last(0); if (can_feed_fire) { if (get_personal_count(ITEM_STICKS) > 0) { - options.insert_last("Feed fire with stick"); + options.insert_last(tr("system.action.option.feed_fire_stick")); action_types.insert_last(1); } if (get_personal_count(ITEM_VINES) > 0) { - options.insert_last("Feed fire with vine"); + options.insert_last(tr("system.action.option.feed_fire_vine")); action_types.insert_last(2); } if (get_personal_count(ITEM_LOGS) > 0) { - options.insert_last("Feed fire with log"); + options.insert_last(tr("system.action.option.feed_fire_log")); action_types.insert_last(3); } } if (x <= BASE_END && world_altars.length() > 0 && get_personal_count(ITEM_INCENSE) > 0) { - options.insert_last("Burn incense"); + options.insert_last(tr("system.action.option.burn_incense")); action_types.insert_last(4); } if (check_fire != null) { - options.insert_last("Check fire"); + options.insert_last(tr("system.action.option.check_fire")); action_types.insert_last(5); } if (check_snare != null) { - options.insert_last("Check snare"); + options.insert_last(tr("system.action.option.check_snare")); action_types.insert_last(6); } if (line_in_water || fish_on_line || is_reeling || is_casting) { - options.insert_last("Check fishing pole"); + options.insert_last(tr("system.action.option.check_fishing_pole")); action_types.insert_last(7); } string filter_text = ""; 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.menu.no_options"), true); + } else { + dictionary promptArgs; + promptArgs.set("option", filtered_options[selection]); + speak_with_history(trf("system.action.menu.prompt", promptArgs), true); + } - while(true) { + while (true) { wait(5); if (menu_background_tick()) { return; } if (key_pressed(KEY_ESCAPE)) { - speak_with_history("Closed.", true); + speak_with_history(tr("system.menu.closed"), true); break; } - bool filter_changed = update_menu_filter_state(filter_text, options, filtered_indices, filtered_options, selection); + bool filter_changed = + update_menu_filter_state(filter_text, options, filtered_indices, filtered_options, selection); 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); @@ -288,7 +318,8 @@ void run_action_menu(int x) { if (filtered_options.length() > 0) { play_menu_move_sound(); selection++; - if (selection >= int(filtered_options.length())) selection = 0; + if (selection >= int(filtered_options.length())) + selection = 0; speak_with_history(filtered_options[selection], true); } } @@ -297,7 +328,8 @@ void run_action_menu(int x) { if (filtered_options.length() > 0) { play_menu_move_sound(); selection--; - if (selection < 0) selection = int(filtered_options.length()) - 1; + if (selection < 0) + selection = int(filtered_options.length()) - 1; speak_with_history(filtered_options[selection], true); } } @@ -335,7 +367,7 @@ void run_action_menu(int x) { play_menu_select_sound(); int action = action_types[filtered_indices[selection]]; if (action == 0) { - speak_with_history("Can't do that.", true); + speak_with_history(tr("system.action.cant_do_that"), true); } else if (action == 1) { try_feed_fire_stick_max(nearby_fire); } else if (action == 2) { diff --git a/src/menus/altar_menu.nvgt b/src/menus/altar_menu.nvgt index fc08af8..a6031e8 100644 --- a/src/menus/altar_menu.nvgt +++ b/src/menus/altar_menu.nvgt @@ -2,22 +2,24 @@ // Functions for sacrificing items at the altar string format_altar_favor(double value) { - if (value < 0) value = 0; + if (value < 0) + value = 0; return "" + int(value); } void check_altar_menu(int player_x) { - if (!key_pressed(KEY_S)) return; + if (!key_pressed(KEY_S)) + return; // Must be in base if (player_x > BASE_END) { - speak_with_history("Must be in base to use altar.", true); + speak_with_history(tr("system.altar.must_be_in_base"), true); return; } // Must have altar if (world_altars.length() == 0) { - speak_with_history("No altar built.", true); + speak_with_history(tr("system.altar.no_altar_built"), true); return; } @@ -26,12 +28,12 @@ void check_altar_menu(int player_x) { void sacrifice_item(int item_type) { if (item_type == -2 || is_runed_item_type(item_type)) { - speak_with_history("Runed items cannot be sacrificed.", true); + speak_with_history(tr("system.altar.runed_cannot_sacrifice"), true); return; } int available = get_personal_count(item_type); if (available <= 0) { - speak_with_history("Nothing to sacrifice.", true); + speak_with_history(tr("system.altar.nothing_to_sacrifice"), true); return; } @@ -40,25 +42,28 @@ void sacrifice_item(int item_type) { if (personal_small_game_types.length() > 0) { personal_small_game_types.remove_at(0); } - } - else { + } else { add_personal_count(item_type, -1); } cleanup_equipment_after_inventory_change(); double favor_gain = get_item_favor_value(item_type); favor += favor_gain; - speak_with_history("Sacrificed 1 " + get_item_label_singular(item_type) + ". Favor +" + format_altar_favor(favor_gain) + ". Total " + format_altar_favor(favor) + ".", true); + dictionary args; + args.set("item", get_item_label_singular(item_type)); + args.set("gain", format_altar_favor(favor_gain)); + args.set("total", format_altar_favor(favor)); + speak_with_history(trf("system.altar.sacrificed_one", args), true); } void sacrifice_item_max(int item_type) { if (item_type == -2 || is_runed_item_type(item_type)) { - speak_with_history("Runed items cannot be sacrificed.", true); + speak_with_history(tr("system.altar.runed_cannot_sacrifice"), true); return; } int available = get_personal_count(item_type); if (available <= 0) { - speak_with_history("Nothing to sacrifice.", true); + speak_with_history(tr("system.altar.nothing_to_sacrifice"), true); return; } @@ -67,8 +72,7 @@ void sacrifice_item_max(int item_type) { if (item_type == ITEM_SMALL_GAME) { set_personal_count(ITEM_SMALL_GAME, 0); personal_small_game_types.resize(0); - } - else { + } else { set_personal_count(item_type, 0); } @@ -76,11 +80,18 @@ void sacrifice_item_max(int item_type) { double total_favor = favor_per_item * available; favor += total_favor; - speak_with_history("Sacrificed " + available + " " + get_item_label(item_type) + ". Favor +" + format_altar_favor(total_favor) + ". Total " + format_altar_favor(favor) + ".", true); + dictionary args; + args.set("count", available); + args.set("items", get_item_label(item_type)); + args.set("gain", format_altar_favor(total_favor)); + args.set("total", format_altar_favor(favor)); + speak_with_history(trf("system.altar.sacrificed_many", args), true); } void run_altar_menu() { - speak_with_history("Altar. Favor " + format_altar_favor(favor) + ".", true); + dictionary favorArgs; + favorArgs.set("favor", format_altar_favor(favor)); + speak_with_history(trf("system.altar.menu.prompt", favorArgs), true); int selection = 0; string[] options; @@ -91,23 +102,26 @@ void run_altar_menu() { string[] filtered_options; apply_menu_filter(filter_text, options, filtered_indices, filtered_options); - while(true) { + while (true) { wait(5); if (menu_background_tick()) { return; } if (key_pressed(KEY_ESCAPE)) { - speak_with_history("Closed.", true); + speak_with_history(tr("system.menu.closed"), true); break; } - bool filter_changed = update_menu_filter_state(filter_text, options, filtered_indices, filtered_options, selection); + bool filter_changed = + update_menu_filter_state(filter_text, options, filtered_indices, filtered_options, selection); 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); @@ -118,7 +132,8 @@ void run_altar_menu() { if (filtered_options.length() > 0) { play_menu_move_sound(); selection++; - if (selection >= int(filtered_options.length())) selection = 0; + if (selection >= int(filtered_options.length())) + selection = 0; speak_with_history(filtered_options[selection], true); } } @@ -127,7 +142,8 @@ void run_altar_menu() { if (filtered_options.length() > 0) { play_menu_move_sound(); selection--; - if (selection < 0) selection = int(filtered_options.length()) - 1; + if (selection < 0) + selection = int(filtered_options.length()) - 1; speak_with_history(filtered_options[selection], true); } } @@ -138,11 +154,14 @@ void run_altar_menu() { sacrifice_item(item_types[filtered_indices[selection]]); build_personal_inventory_options(options, item_types); apply_menu_filter(filter_text, options, filtered_indices, filtered_options); - if (selection >= int(filtered_options.length())) selection = 0; + if (selection >= int(filtered_options.length())) + selection = 0; 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); + dictionary filterArgs; + filterArgs.set("arg1", filter_text); + speak_with_history(trf("system.menu.no_matches", filterArgs), true); } } } @@ -153,11 +172,14 @@ void run_altar_menu() { sacrifice_item_max(item_types[filtered_indices[selection]]); build_personal_inventory_options(options, item_types); apply_menu_filter(filter_text, options, filtered_indices, filtered_options); - if (selection >= int(filtered_options.length())) selection = 0; + if (selection >= int(filtered_options.length())) + selection = 0; 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); + dictionary filterArgs; + filterArgs.set("arg1", filter_text); + speak_with_history(trf("system.menu.no_matches", filterArgs), true); } } } diff --git a/src/menus/base_info.nvgt b/src/menus/base_info.nvgt index f9077eb..9a79fee 100644 --- a/src/menus/base_info.nvgt +++ b/src/menus/base_info.nvgt @@ -3,7 +3,7 @@ void run_base_info_menu() { if (x > BASE_END) { - speak_with_history("You are not in the base.", true); + speak_with_history(tr("system.base_info.not_in_base"), true); return; } @@ -23,18 +23,24 @@ void run_base_info_menu() { int smoked_fish_in_storage = get_storage_count(ITEM_SMOKED_FISH); int basket_food_in_storage = get_storage_count(ITEM_BASKET_FOOD); int total_food = meat_in_storage + smoked_fish_in_storage + basket_food_in_storage; - options.insert_last("Food in storage " + meat_in_storage + " meat, " + smoked_fish_in_storage + " smoked fish, " + basket_food_in_storage + " baskets of fruits and nuts. Total " + total_food + ". Daily use " + daily_food); + options.insert_last("Food in storage " + meat_in_storage + " meat, " + smoked_fish_in_storage + + " smoked fish, " + basket_food_in_storage + " baskets of fruits and nuts. Total " + + total_food + ". Daily use " + daily_food); } else { options.insert_last("Storage not built"); } options.insert_last(get_base_fire_status()); - if (world_altars.length() > 0) options.insert_last("Altar built"); - else options.insert_last("Altar not built"); + if (world_altars.length() > 0) + options.insert_last("Altar built"); + else + options.insert_last("Altar not built"); - if (get_herb_garden_at_base() != null) options.insert_last("Herb garden built"); - else options.insert_last("Herb garden not built"); + if (get_herb_garden_at_base() != null) + options.insert_last("Herb garden built"); + else + options.insert_last("Herb garden not built"); if (world_pastures.length() > 0) { options.insert_last("Pasture built. Livestock " + livestock_count + " of " + MAX_LIVESTOCK); @@ -47,33 +53,39 @@ 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; apply_menu_filter(filter_text, options, filtered_indices, filtered_options); if (filtered_options.length() == 0) { - speak_with_history("Base info. No options.", true); + speak_with_history(tr("system.base_info.menu.no_options"), true); } else { - speak_with_history("Base info. " + filtered_options[selection], true); + dictionary promptArgs; + promptArgs.set("option", filtered_options[selection]); + speak_with_history(trf("system.base_info.menu.prompt", promptArgs), true); } - while(true) { + while (true) { wait(5); if (menu_background_tick()) { return; } if (key_pressed(KEY_ESCAPE)) { - speak_with_history("Closed.", true); + speak_with_history(tr("system.menu.closed"), true); break; } - bool filter_changed = update_menu_filter_state(filter_text, options, filtered_indices, filtered_options, selection); + bool filter_changed = + update_menu_filter_state(filter_text, options, filtered_indices, filtered_options, selection); 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); @@ -84,7 +96,8 @@ void run_base_info_menu() { if (filtered_options.length() > 0) { play_menu_move_sound(); selection++; - if (selection >= int(filtered_options.length())) selection = 0; + if (selection >= int(filtered_options.length())) + selection = 0; speak_with_history(filtered_options[selection], true); } } @@ -93,7 +106,8 @@ void run_base_info_menu() { if (filtered_options.length() > 0) { play_menu_move_sound(); selection--; - if (selection < 0) selection = int(filtered_options.length()) - 1; + if (selection < 0) + selection = int(filtered_options.length()) - 1; speak_with_history(filtered_options[selection], true); } } diff --git a/src/menus/character_info.nvgt b/src/menus/character_info.nvgt index cffff12..a47243b 100644 --- a/src/menus/character_info.nvgt +++ b/src/menus/character_info.nvgt @@ -1,53 +1,154 @@ // Character info display // Functions for displaying character stats and equipment +bool run_pet_abandon_confirm_menu() { + if (!petActive) { + speak_with_history(tr("system.character.pet.no_pet"), true); + return false; + } + + string[] options; + options.insert_last(tr("system.option.no")); + options.insert_last(tr("system.option.yes")); + int selection = 0; + + dictionary promptArgs; + promptArgs.set("option", options[selection]); + speak_with_history(trf("system.character.pet.abandon_confirm", promptArgs), true); + + while (true) { + wait(5); + if (menu_background_tick()) { + return false; + } + if (key_pressed(KEY_ESCAPE)) { + speak_with_history(tr("system.menu.canceled"), true); + return false; + } + if (key_pressed(KEY_DOWN)) { + play_menu_move_sound(); + selection++; + if (selection >= int(options.length())) + selection = 0; + speak_with_history(options[selection], true); + } + if (key_pressed(KEY_UP)) { + play_menu_move_sound(); + selection--; + if (selection < 0) + selection = int(options.length()) - 1; + speak_with_history(options[selection], true); + } + if (key_pressed(KEY_RETURN)) { + play_menu_select_sound(); + if (selection == 1) { + return abandon_pet(); + } + speak_with_history(tr("system.menu.canceled"), true); + return false; + } + } + + return false; +} + void run_character_info_menu() { string[] equipped_clothing; string[] missing_slots; - if (equipped_head == EQUIP_HAT) equipped_clothing.insert_last("Skin Hat"); - else missing_slots.insert_last("head"); - if (equipped_torso == EQUIP_TUNIC) equipped_clothing.insert_last("Skin Tunic"); - else missing_slots.insert_last("torso"); - if (equipped_arms == EQUIP_POUCH) equipped_clothing.insert_last("Skin Pouch"); - else if (equipped_arms == EQUIP_BACKPACK) equipped_clothing.insert_last("Backpack"); - else missing_slots.insert_last("arms"); - if (equipped_hands == EQUIP_GLOVES) equipped_clothing.insert_last("Skin Gloves"); - else missing_slots.insert_last("hands"); - if (equipped_legs == EQUIP_PANTS) equipped_clothing.insert_last("Skin Pants"); - else missing_slots.insert_last("legs"); - if (equipped_feet == EQUIP_MOCCASINS) equipped_clothing.insert_last("Moccasins"); - else missing_slots.insert_last("feet"); + if (equipped_head == EQUIP_HAT) + equipped_clothing.insert_last(get_base_equipment_name(EQUIP_HAT)); + else + missing_slots.insert_last(tr("system.character.slot.head")); + if (equipped_torso == EQUIP_TUNIC) + equipped_clothing.insert_last(get_base_equipment_name(EQUIP_TUNIC)); + else + missing_slots.insert_last(tr("system.character.slot.torso")); + if (equipped_arms == EQUIP_POUCH) + equipped_clothing.insert_last(get_base_equipment_name(EQUIP_POUCH)); + else if (equipped_arms == EQUIP_BACKPACK) + equipped_clothing.insert_last(get_base_equipment_name(EQUIP_BACKPACK)); + else + missing_slots.insert_last(tr("system.character.slot.arms")); + if (equipped_hands == EQUIP_GLOVES) + equipped_clothing.insert_last(get_base_equipment_name(EQUIP_GLOVES)); + else + missing_slots.insert_last(tr("system.character.slot.hands")); + if (equipped_legs == EQUIP_PANTS) + equipped_clothing.insert_last(get_base_equipment_name(EQUIP_PANTS)); + else + missing_slots.insert_last(tr("system.character.slot.legs")); + if (equipped_feet == EQUIP_MOCCASINS) + equipped_clothing.insert_last(get_base_equipment_name(EQUIP_MOCCASINS)); + else + 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; @@ -56,28 +157,33 @@ 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) { + while (true) { wait(5); if (menu_background_tick()) { return; } if (key_pressed(KEY_ESCAPE)) { - speak_with_history("Closed.", true); + speak_with_history(tr("system.menu.closed"), true); break; } - bool filter_changed = update_menu_filter_state(filter_text, options, filtered_indices, filtered_options, selection); + bool filter_changed = + update_menu_filter_state(filter_text, options, filtered_indices, filtered_options, selection); 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); @@ -88,7 +194,8 @@ void run_character_info_menu() { if (filtered_options.length() > 0) { play_menu_move_sound(); selection++; - if (selection >= int(filtered_options.length())) selection = 0; + if (selection >= int(filtered_options.length())) + selection = 0; speak_with_history(filtered_options[selection], true); } } @@ -97,10 +204,36 @@ void run_character_info_menu() { if (filtered_options.length() > 0) { play_menu_move_sound(); selection--; - if (selection < 0) selection = int(filtered_options.length()) - 1; + if (selection < 0) + selection = int(filtered_options.length()) - 1; speak_with_history(filtered_options[selection], true); } } + + if (key_pressed(KEY_RETURN)) { + if (filtered_options.length() == 0) + continue; + + int optionIndex = filtered_indices[selection]; + if (optionIndex == petOptionIndex && petActive) { + if (is_pet_knocked_out()) { + dictionary args; + args.set("pet", get_pet_display_name()); + speak_with_history(trf("system.character.pet.unconscious_cannot_abandon", args), true); + continue; + } + + play_menu_select_sound(); + if (run_pet_abandon_confirm_menu()) { + return; + } + if (selection >= int(filtered_options.length())) + selection = int(filtered_options.length()) - 1; + if (selection >= 0 && selection < int(filtered_options.length())) { + speak_with_history(filtered_options[selection], true); + } + } + } } } diff --git a/src/menus/equipment_menu.nvgt b/src/menus/equipment_menu.nvgt index fdfd849..a2fb2e4 100644 --- a/src/menus/equipment_menu.nvgt +++ b/src/menus/equipment_menu.nvgt @@ -6,11 +6,10 @@ bool has_any_equipment() { // Check unruned items if (get_personal_count(ITEM_SPEARS) > 0 || get_personal_count(ITEM_AXES) > 0 || get_personal_count(ITEM_SLINGS) > 0 || get_personal_count(ITEM_BOWS) > 0 || - get_personal_count(ITEM_FISHING_POLES) > 0 || - get_personal_count(ITEM_SKIN_HATS) > 0 || get_personal_count(ITEM_SKIN_GLOVES) > 0 || - get_personal_count(ITEM_SKIN_PANTS) > 0 || get_personal_count(ITEM_SKIN_TUNICS) > 0 || - get_personal_count(ITEM_MOCCASINS) > 0 || get_personal_count(ITEM_SKIN_POUCHES) > 0 || - get_personal_count(ITEM_BACKPACKS) > 0) { + get_personal_count(ITEM_FISHING_POLES) > 0 || get_personal_count(ITEM_SKIN_HATS) > 0 || + get_personal_count(ITEM_SKIN_GLOVES) > 0 || get_personal_count(ITEM_SKIN_PANTS) > 0 || + get_personal_count(ITEM_SKIN_TUNICS) > 0 || get_personal_count(ITEM_MOCCASINS) > 0 || + get_personal_count(ITEM_SKIN_POUCHES) > 0 || get_personal_count(ITEM_BACKPACKS) > 0) { return true; } @@ -32,7 +31,7 @@ bool has_any_equipment() { void check_equipment_menu() { if (key_pressed(KEY_E)) { if (!has_any_equipment()) { - speak_with_history("Nothing to equip.", true); + speak_with_history(tr("system.equipment.menu.nothing_to_equip"), true); } else { run_equipment_menu(); } @@ -50,13 +49,18 @@ string get_full_equipment_name(int equip_type, int rune_type) { // Check if an item with specific rune is equipped bool is_runed_item_equipped(int equip_type, int rune_type) { - if (!equipment_is_equipped(equip_type)) return false; + if (!equipment_is_equipped(equip_type)) + return false; return get_equipped_rune_for_slot(equip_type) == rune_type; } -void run_equipment_menu() { - speak_with_history("Equipment menu.", true); +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() { int selection = 0; string[] options; int[] equipment_types; @@ -64,74 +68,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); } @@ -145,9 +149,10 @@ void run_equipment_menu() { for (uint j = 0; j < all_rune_types.length(); j++) { int rune_type = all_rune_types[j]; int count = get_runed_item_count(equip_type, rune_type); - if (count <= 0) continue; + 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); @@ -157,24 +162,34 @@ void run_equipment_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.menu.no_options"), true); + } else { + dictionary promptArgs; + promptArgs.set("option", filtered_options[selection]); + speak_with_history(trf("system.equipment.menu.prompt", promptArgs), true); + } - while(true) { + while (true) { wait(5); if (menu_background_tick()) { return; } if (key_pressed(KEY_ESCAPE)) { - speak_with_history("Closed.", true); + speak_with_history(tr("system.menu.closed"), true); break; } - bool filter_changed = update_menu_filter_state(filter_text, options, filtered_indices, filtered_options, selection); + bool filter_changed = + update_menu_filter_state(filter_text, options, filtered_indices, filtered_options, selection); 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); @@ -185,7 +200,8 @@ void run_equipment_menu() { if (filtered_options.length() > 0) { play_menu_move_sound(); selection++; - if (selection >= int(filtered_options.length())) selection = 0; + if (selection >= int(filtered_options.length())) + selection = 0; speak_with_history(filtered_options[selection], true); } } @@ -194,7 +210,8 @@ void run_equipment_menu() { if (filtered_options.length() > 0) { play_menu_move_sound(); selection--; - if (selection < 0) selection = int(filtered_options.length()) - 1; + if (selection < 0) + selection = int(filtered_options.length()) - 1; speak_with_history(filtered_options[selection], true); } } @@ -208,7 +225,10 @@ void run_equipment_menu() { quick_slot_runes[slot_index] = rune_type; } string name = get_full_equipment_name(equip_type, rune_type); - speak_with_history(name + " set to slot " + slot_index + ".", true); + dictionary args; + args.set("name", name); + args.set("slot", slot_index); + speak_with_history(trf("system.equipment.set_to_slot", args), true); } if (key_pressed(KEY_RETURN)) { @@ -224,12 +244,16 @@ void run_equipment_menu() { // Unequip unequip_equipment_type(equip_type); clear_equipped_rune_for_slot(equip_type); - speak_with_history(name + " unequipped.", true); + dictionary unequip_args; + unequip_args.set("name", name); + speak_with_history(trf("system.equipment.unequipped", unequip_args), true); } else { // Equip equip_equipment_type(equip_type); set_equipped_rune_for_slot(equip_type, rune_type); - speak_with_history(name + " equipped.", true); + dictionary equip_args; + equip_args.set("name", name); + speak_with_history(trf("system.equipment.equipped", equip_args), true); } update_max_health_from_equipment(); break; diff --git a/src/menus/inventory_core.nvgt b/src/menus/inventory_core.nvgt index cd1063f..48653c1 100644 --- a/src/menus/inventory_core.nvgt +++ b/src/menus/inventory_core.nvgt @@ -1,7 +1,19 @@ // Personal inventory menu system // Functions for displaying and managing personal inventory -void build_personal_inventory_options(string[]@ options, int[]@ item_types) { +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); @@ -23,7 +35,8 @@ void build_personal_inventory_options(string[]@ options, int[]@ item_types) { for (uint j = 0; j < rune_types.length(); j++) { int rune_type = rune_types[j]; int count = get_runed_item_count(equip_type, rune_type); - if (count <= 0) continue; + if (count <= 0) + continue; string name = get_runed_item_display_name(equip_type, rune_type); options.insert_last(name + ": " + count); item_types.insert_last(encode_runed_item_type(equip_type, rune_type)); @@ -40,7 +53,8 @@ void show_inventory() { int item_type = inventory_display_order[i]; int count = get_personal_count(item_type); if (count > 0) { - if (!first) info += ", "; + if (!first) + info += ", "; info += count + " " + get_item_label(item_type); first = false; } @@ -60,8 +74,10 @@ void show_inventory() { for (uint j = 0; j < rune_types.length(); j++) { int rune_type = rune_types[j]; int count = get_runed_item_count(equip_type, rune_type); - if (count <= 0) continue; - if (runed_info.length() > 0) runed_info += ", "; + if (count <= 0) + continue; + if (runed_info.length() > 0) + runed_info += ", "; runed_info += count + " " + get_runed_item_display_name(equip_type, rune_type); } } @@ -74,47 +90,48 @@ 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) { + while (true) { wait(5); if (menu_background_tick()) { return; } if (key_pressed(KEY_ESCAPE)) { - speak_with_history("Closed.", true); + speak_with_history(tr("system.menu.closed"), true); break; } 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); } if (key_pressed(KEY_RETURN)) { play_menu_select_sound(); - if (selection == 0) run_inventory_menu(true); - else run_storage_menu(); + if (selection == 0) + run_inventory_menu(true); + else + run_storage_menu(); break; } } } void run_inventory_menu(bool allow_deposit) { - speak_with_history("Inventory menu.", true); - int selection = 0; string[] options; int[] item_types; @@ -123,24 +140,30 @@ 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) { + while (true) { wait(5); if (menu_background_tick()) { return; } if (key_pressed(KEY_ESCAPE)) { - speak_with_history("Closed.", true); + speak_with_history(tr("system.menu.closed"), true); break; } - bool filter_changed = update_menu_filter_state(filter_text, options, filtered_indices, filtered_options, selection); + bool filter_changed = + update_menu_filter_state(filter_text, options, filtered_indices, filtered_options, selection); 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); @@ -151,7 +174,8 @@ void run_inventory_menu(bool allow_deposit) { if (filtered_options.length() > 0) { play_menu_move_sound(); selection++; - if (selection >= int(filtered_options.length())) selection = 0; + if (selection >= int(filtered_options.length())) + selection = 0; speak_with_history(filtered_options[selection], true); } } @@ -160,7 +184,8 @@ void run_inventory_menu(bool allow_deposit) { if (filtered_options.length() > 0) { play_menu_move_sound(); selection--; - if (selection < 0) selection = int(filtered_options.length()) - 1; + if (selection < 0) + selection = int(filtered_options.length()) - 1; speak_with_history(filtered_options[selection], true); } } @@ -171,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); } } @@ -181,11 +209,12 @@ void run_inventory_menu(bool allow_deposit) { deposit_item(item_types[filtered_indices[selection]]); build_personal_inventory_options(options, item_types); apply_menu_filter(filter_text, options, filtered_indices, filtered_options); - if (selection >= int(filtered_options.length())) selection = 0; + if (selection >= int(filtered_options.length())) + selection = 0; 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); } } } @@ -196,11 +225,12 @@ void run_inventory_menu(bool allow_deposit) { deposit_item_max(item_types[filtered_indices[selection]]); build_personal_inventory_options(options, item_types); apply_menu_filter(filter_text, options, filtered_indices, filtered_options); - if (selection >= int(filtered_options.length())) selection = 0; + if (selection >= int(filtered_options.length())) + selection = 0; 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/menu_utils.nvgt b/src/menus/menu_utils.nvgt index 3410335..8f1c15d 100644 --- a/src/menus/menu_utils.nvgt +++ b/src/menus/menu_utils.nvgt @@ -28,29 +28,33 @@ bool menu_background_tick() { 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); + dictionary burningArgs; + burningArgs.set("health", player_health); + speak_with_history(trf("system.combat.burning", burningArgs), true); } // Healing in base area if (x <= BASE_END && player_health < max_health) { - WorldHerbGarden@ herb_garden = get_herb_garden_at_base(); + 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); + dictionary healthArgs; + healthArgs.set("health", player_health); + speak_with_history(trf("system.combat.health_status", healthArgs), true); } } // Death check if (player_health <= 0) { if (!try_consume_heal_scroll()) { - speak_with_history("You have died.", true); + speak_with_history(tr("system.combat.you_died"), true); wait(2000); return_to_main_menu_requested = true; return true; @@ -62,18 +66,21 @@ bool menu_background_tick() { void play_menu_move_sound() { string soundFile = "sounds/menu/menu_move.ogg"; - if (!file_exists(soundFile)) return; + if (!audio_asset_exists(soundFile)) + return; p.play_stationary(soundFile, false); } void play_menu_select_sound() { string soundFile = "sounds/menu/menu_select.ogg"; - if (!file_exists(soundFile)) return; + if (!audio_asset_exists(soundFile)) + return; p.play_stationary(soundFile, false); } -string join_string_list(const string[]@ items) { - if (@items == null || items.length() == 0) return ""; +string join_string_list(const string[] @items) { + if (@items == null || items.length() == 0) + return ""; string result = items[0]; for (uint i = 1; i < items.length(); i++) { result += ", " + items[i]; @@ -100,7 +107,8 @@ string get_base_fire_status() { } } } - if (total == 0) return "No fires in base"; + if (total == 0) + return "No fires in base"; return "Fires in base: " + burning + " burning, " + total + " total"; } @@ -108,10 +116,12 @@ string get_menu_filter_letter() { return menu_get_filter_letter(); } -void apply_menu_filter(const string &in filter_text, const string[]@ options, int[]@ filtered_indices, string[]@ filtered_options) { +void apply_menu_filter(const string& in filter_text, const string[] @options, int[] @filtered_indices, + string[] @filtered_options) { menu_apply_prefix_filter(filter_text, options, filtered_indices, filtered_options); } -bool update_menu_filter_state(string &inout filter_text, const string[]@ options, int[]@ filtered_indices, string[]@ filtered_options, int &inout selection) { +bool update_menu_filter_state(string& inout filter_text, const string[] @options, int[] @filtered_indices, + string[] @filtered_options, int& inout selection) { return menu_update_prefix_filter(filter_text, options, filtered_indices, filtered_options, selection); } diff --git a/src/menus/storage_menu.nvgt b/src/menus/storage_menu.nvgt index 90cac2d..197169c 100644 --- a/src/menus/storage_menu.nvgt +++ b/src/menus/storage_menu.nvgt @@ -1,9 +1,28 @@ // 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); +} + +string get_storage_runed_item_name(int equip_type, int rune_type) { + dictionary args; + args.set("equipment", get_base_equipment_name(equip_type)); + args.set("rune", get_rune_effect_name(rune_type)); + return trf("system.storage.runed_item_name", args); +} + void move_small_game_to_storage(int amount) { for (int i = 0; i < amount; i++) { - string game_type = "small game"; + string game_type = get_item_label_singular(ITEM_SMALL_GAME); if (personal_small_game_types.length() > 0) { game_type = personal_small_game_types[0]; personal_small_game_types.remove_at(0); @@ -14,7 +33,7 @@ void move_small_game_to_storage(int amount) { void move_small_game_to_personal(int amount) { for (int i = 0; i < amount; i++) { - string game_type = "small game"; + string game_type = get_item_label_singular(ITEM_SMALL_GAME); if (storage_small_game_types.length() > 0) { game_type = storage_small_game_types[0]; storage_small_game_types.remove_at(0); @@ -31,11 +50,16 @@ 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; - if (amount > max_amount) amount = max_amount; + if (amount <= 0) + return 0; + if (amount > max_amount) + amount = max_amount; return amount; } @@ -45,32 +69,35 @@ void deposit_item(int item_type) { int equip_type, rune_type; decode_runed_item_type(item_type, equip_type, rune_type); if (deposit_runed_item(equip_type, rune_type)) { - string name = "Runed " + get_base_equipment_name(equip_type) + " of " + get_rune_effect_name(rune_type); + string name = get_storage_runed_item_name(equip_type, rune_type); cleanup_equipment_after_inventory_change(); - speak_with_history("Deposited " + name + ".", true); + dictionary args; + args.set("name", name); + speak_with_history(trf("system.storage.deposited_one", args), 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); - if (amount <= 0) return; + int amount = prompt_transfer_amount(tr("system.storage.deposit_how_many"), max_transfer); + if (amount <= 0) + return; // Transfer the items add_personal_count(item_type, -amount); @@ -85,7 +112,10 @@ void deposit_item(int item_type) { } cleanup_equipment_after_inventory_change(); - speak_with_history("Deposited " + amount + " " + get_item_label(item_type) + ".", true); + dictionary args; + args.set("amount", amount); + args.set("item", get_item_label(item_type)); + speak_with_history(trf("system.storage.deposited_many", args), true); } void deposit_item_max(int item_type) { @@ -95,7 +125,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; @@ -103,25 +133,28 @@ void deposit_item_max(int item_type) { deposit_runed_item(equip_type, rune_type); count++; } - string name = "Runed " + get_base_equipment_name(equip_type) + " of " + get_rune_effect_name(rune_type); + string name = get_storage_runed_item_name(equip_type, rune_type); cleanup_equipment_after_inventory_change(); - speak_with_history("Deposited " + count + " " + name + ".", true); + dictionary args; + args.set("amount", count); + args.set("item", name); + speak_with_history(trf("system.storage.deposited_many", args), true); return; } // 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; } @@ -140,7 +173,10 @@ void deposit_item_max(int item_type) { } cleanup_equipment_after_inventory_change(); - speak_with_history("Deposited " + amount + " " + get_item_label(item_type) + ".", true); + dictionary args; + args.set("amount", amount); + args.set("item", get_item_label(item_type)); + speak_with_history(trf("system.storage.deposited_many", args), true); } void withdraw_item(int item_type) { @@ -149,35 +185,38 @@ void withdraw_item(int item_type) { int equip_type, rune_type; decode_runed_item_type(item_type, equip_type, rune_type); if (withdraw_runed_item(equip_type, rune_type)) { - string name = "Runed " + get_base_equipment_name(equip_type) + " of " + get_rune_effect_name(rune_type); - speak_with_history("Withdrew " + name + ".", true); + string name = get_storage_runed_item_name(equip_type, rune_type); + dictionary args; + args.set("name", name); + speak_with_history(trf("system.storage.withdrew_one", args), 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); - if (amount <= 0) return; + int amount = prompt_transfer_amount(tr("system.storage.withdraw_how_many"), max_transfer); + if (amount <= 0) + return; // Transfer the items add_storage_count(item_type, -amount); @@ -191,7 +230,10 @@ void withdraw_item(int item_type) { move_fish_to_personal(amount); } - speak_with_history("Withdrew " + amount + " " + get_item_label(item_type) + ".", true); + dictionary args; + args.set("amount", amount); + args.set("item", get_item_label(item_type)); + speak_with_history(trf("system.storage.withdrew_many", args), true); } void withdraw_item_max(int item_type) { @@ -201,7 +243,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; @@ -209,13 +251,16 @@ void withdraw_item_max(int item_type) { withdraw_runed_item(equip_type, rune_type); count++; } - string name = "Runed " + get_base_equipment_name(equip_type) + " of " + get_rune_effect_name(rune_type); - speak_with_history("Withdrew " + count + " " + name + ".", true); + string name = get_storage_runed_item_name(equip_type, rune_type); + dictionary args; + args.set("amount", count); + args.set("item", name); + speak_with_history(trf("system.storage.withdrew_many", args), 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; } @@ -225,7 +270,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 { @@ -235,7 +280,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; } @@ -253,10 +298,13 @@ void withdraw_item_max(int item_type) { move_fish_to_personal(amount); } - speak_with_history("Withdrew " + amount + " " + get_item_label(item_type) + ".", true); + dictionary args; + args.set("amount", amount); + args.set("item", get_item_label(item_type)); + speak_with_history(trf("system.storage.withdrew_many", args), true); } -void build_storage_inventory_options(string[]@ options, int[]@ item_types) { +void build_storage_inventory_options(string[] @options, int[] @item_types) { options.resize(0); item_types.resize(0); @@ -278,7 +326,8 @@ void build_storage_inventory_options(string[]@ options, int[]@ item_types) { for (uint j = 0; j < rune_types.length(); j++) { int rune_type = rune_types[j]; int count = get_stored_runed_item_count(equip_type, rune_type); - if (count <= 0) continue; + if (count <= 0) + continue; string name = get_runed_item_display_name(equip_type, rune_type); options.insert_last(name + ": " + count); item_types.insert_last(encode_runed_item_type(equip_type, rune_type)); @@ -288,12 +337,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; @@ -302,24 +349,30 @@ 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) { + while (true) { wait(5); if (menu_background_tick()) { return; } if (key_pressed(KEY_ESCAPE)) { - speak_with_history("Closed.", true); + speak_with_history(tr("system.menu.closed"), true); break; } - bool filter_changed = update_menu_filter_state(filter_text, options, filtered_indices, filtered_options, selection); + bool filter_changed = + update_menu_filter_state(filter_text, options, filtered_indices, filtered_options, selection); 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); @@ -330,7 +383,8 @@ void run_storage_menu() { if (filtered_options.length() > 0) { play_menu_move_sound(); selection++; - if (selection >= int(filtered_options.length())) selection = 0; + if (selection >= int(filtered_options.length())) + selection = 0; speak_with_history(filtered_options[selection], true); } } @@ -339,7 +393,8 @@ void run_storage_menu() { if (filtered_options.length() > 0) { play_menu_move_sound(); selection--; - if (selection < 0) selection = int(filtered_options.length()) - 1; + if (selection < 0) + selection = int(filtered_options.length()) - 1; speak_with_history(filtered_options[selection], true); } } @@ -350,11 +405,12 @@ void run_storage_menu() { withdraw_item(item_types[filtered_indices[selection]]); build_storage_inventory_options(options, item_types); apply_menu_filter(filter_text, options, filtered_indices, filtered_options); - if (selection >= int(filtered_options.length())) selection = 0; + if (selection >= int(filtered_options.length())) + selection = 0; 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); } } } @@ -365,11 +421,12 @@ void run_storage_menu() { withdraw_item_max(item_types[filtered_indices[selection]]); build_storage_inventory_options(options, item_types); apply_menu_filter(filter_text, options, filtered_indices, filtered_options); - if (selection >= int(filtered_options.length())) selection = 0; + if (selection >= int(filtered_options.length())) + selection = 0; 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/notify_compat.nvgt b/src/notify_compat.nvgt index c61ed71..397fa2b 100644 --- a/src/notify_compat.nvgt +++ b/src/notify_compat.nvgt @@ -6,12 +6,13 @@ const string NOTIFICATION_SOUND_PATH = "sounds/notify"; bool notificationsCompatConfigured = false; -void notify_compat_speak(const string &in message, bool interrupt) { +void notify_compat_speak(const string& in message, bool interrupt) { speak_with_history(message, interrupt); } void configure_notification_compat_if_needed() { - if (notificationsCompatConfigured) return; + if (notificationsCompatConfigured) + return; notifications_set_max_history(NOTIFICATION_HISTORY_LIMIT); notifications_set_delay_ms(NOTIFICATION_DELAY_MS); diff --git a/src/pet_system.nvgt b/src/pet_system.nvgt index b73663b..89594f9 100644 --- a/src/pet_system.nvgt +++ b/src/pet_system.nvgt @@ -43,7 +43,8 @@ string petLeavingName = ""; string[] petSoundPaths; bool petSoundsInitialized = false; -funcdef bool PetAdventureFindTargetCallback(int originPos, int referencePos, int &out targetPos, string &out targetLabel, int &out targetKind); +funcdef bool PetAdventureFindTargetCallback(int originPos, int referencePos, int& out targetPos, + string& out targetLabel, int& out targetKind); funcdef bool PetAdventureDamageCallback(int targetKind, int targetPos, int damage); bool petAdventureMode = false; @@ -51,14 +52,14 @@ int petAdventurePlayerPos = -1; bool petAdventurePreOut = false; int petAdventurePrePosition = 0; bool petAdventurePrePositionValid = false; -PetAdventureFindTargetCallback@ petAdventureFindTarget = null; -PetAdventureDamageCallback@ petAdventureDamageTarget = null; +PetAdventureFindTargetCallback @petAdventureFindTarget = null; +PetAdventureDamageCallback @petAdventureDamageTarget = null; -string normalize_pet_path(const string&in path) { +string normalize_pet_path(const string& in path) { return path.replace("\\", "/", true); } -string collapse_pet_spaces(const string&in text) { +string collapse_pet_spaces(const string& in text) { string result = text; while (result.find_first(" ") > -1) { result = result.replace(" ", " ", true); @@ -66,7 +67,7 @@ string collapse_pet_spaces(const string&in text) { return result; } -string normalize_pet_name_text(const string&in raw) { +string normalize_pet_name_text(const string& in raw) { string name = raw; name = name.replace("_", " ", true); name = name.replace("-", " ", true); @@ -75,15 +76,15 @@ string normalize_pet_name_text(const string&in raw) { return name; } -void gather_pet_sound_files(const string&in basePath, string[]@ outFiles) { - string[]@ files = find_files(basePath + "/*.ogg"); +void gather_pet_sound_files(const string& in basePath, string[] @outFiles) { + string[] @files = find_files(basePath + "/*.ogg"); if (@files !is null) { for (uint i = 0; i < files.length(); i++) { outFiles.insert_last(basePath + "/" + files[i]); } } - string[]@ folders = find_directories(basePath + "/*"); + string[] @folders = find_directories(basePath + "/*"); if (@folders !is null) { for (uint i = 0; i < folders.length(); i++) { gather_pet_sound_files(basePath + "/" + folders[i], outFiles); @@ -91,22 +92,24 @@ void gather_pet_sound_files(const string&in basePath, string[]@ outFiles) { } } -bool sort_pet_sound_paths(const string &in a, const string &in b) { +bool sort_pet_sound_paths(const string& in a, const string& in b) { return a.lower() < b.lower(); } void init_pet_sounds() { - if (petSoundsInitialized) return; + if (petSoundsInitialized) + return; petSoundsInitialized = true; petSoundPaths.resize(0); - if (!directory_exists("sounds/pets")) return; + if (!directory_exists("sounds/pets")) + return; gather_pet_sound_files("sounds/pets", petSoundPaths); if (petSoundPaths.length() > 1) { petSoundPaths.sort(sort_pet_sound_paths); } } -string get_pet_name_from_sound_path(const string&in soundPath) { +string get_pet_name_from_sound_path(const string& in soundPath) { string normalizedPath = normalize_pet_path(soundPath); int slashPos = normalizedPath.find_last_of("/"); string name = (slashPos >= 0) ? normalizedPath.substr(slashPos + 1) : normalizedPath; @@ -119,13 +122,15 @@ string get_pet_name_from_sound_path(const string&in soundPath) { name = collapse_pet_spaces(name); name = name.lower(); name.trim_whitespace_this(); - if (name.length() == 0) return "Pet"; + if (name.length() == 0) + return "Pet"; string first = name.substr(0, 1).upper(); - if (name.length() == 1) return first; + if (name.length() == 1) + return first; return first + name.substr(1); } -string get_pet_sound_for_name(const string&in petName) { +string get_pet_sound_for_name(const string& in petName) { init_pet_sounds(); string target = petName.lower(); for (uint i = 0; i < petSoundPaths.length(); i++) { @@ -144,17 +149,13 @@ string get_pet_sound_for_name(const string&in petName) { } int get_pet_food_personal_total() { - return get_personal_count(ITEM_MEAT) - + get_personal_count(ITEM_SMOKED_FISH) - + get_personal_count(ITEM_FISH) - + get_personal_count(ITEM_BASKET_FOOD); + return get_personal_count(ITEM_MEAT) + get_personal_count(ITEM_SMOKED_FISH) + get_personal_count(ITEM_FISH) + + get_personal_count(ITEM_BASKET_FOOD); } int get_pet_food_storage_total() { - return get_storage_count(ITEM_MEAT) - + get_storage_count(ITEM_SMOKED_FISH) - + get_storage_count(ITEM_FISH) - + get_storage_count(ITEM_BASKET_FOOD); + return get_storage_count(ITEM_MEAT) + get_storage_count(ITEM_SMOKED_FISH) + get_storage_count(ITEM_FISH) + + get_storage_count(ITEM_BASKET_FOOD); } bool has_pet_food_available() { @@ -199,9 +200,11 @@ bool consume_pet_food() { return false; } -void queue_pet_event(const string&in message, int soundPos = -1, bool playSound = true) { - if (!petActive) return; - if (message.length() == 0) return; +void queue_pet_event(const string& in message, int soundPos = -1, bool playSound = true) { + if (!petActive) + return; + if (message.length() == 0) + return; petEventMessages.insert_last(message); if (playSound) { petEventSounds.insert_last(petSoundPath); @@ -213,26 +216,38 @@ void queue_pet_event(const string&in message, int soundPos = -1, bool playSound string get_pet_display_name() { string name = normalize_pet_name_text(petType); - if (name == "") return "pet"; + if (name == "") + return tr("system.pet.default_name"); return name; } int get_pet_listener_pos() { - if (petAdventurePlayerPos >= 0) return petAdventurePlayerPos; + if (petAdventurePlayerPos >= 0) + return petAdventurePlayerPos; return x; } 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; } int get_pet_search_origin() { - if (petOut && petPositionValid) return petPosition; + if (petOut && petPositionValid) + return petPosition; return get_pet_listener_pos(); } @@ -241,14 +256,16 @@ bool is_hawk_pet() { } void play_pet_travel_step_sound(int stepPos) { - if (stepPos < 0 || stepPos >= MAP_SIZE) return; + if (stepPos < 0 || stepPos >= MAP_SIZE) + return; int listenerPos = get_pet_listener_pos(); - if (abs(stepPos - listenerPos) > CREATURE_DEFAULT_FOOTSTEP_DISTANCE) return; + if (abs(stepPos - listenerPos) > CREATURE_DEFAULT_FOOTSTEP_DISTANCE) + return; if (is_hawk_pet()) { string flySoundPath = "sounds/terrain/fly.ogg"; - if (file_exists(flySoundPath)) { + if (audio_asset_exists(flySoundPath)) { play_1d_with_volume_step(flySoundPath, listenerPos, stepPos, false, CREATURE_DEFAULT_VOLUME_STEP); return; } @@ -264,13 +281,15 @@ void update_pet_travel_position(int currentPos) { petPositionValid = true; previousPos = petPosition; } - if (currentPos == previousPos) return; + if (currentPos == previousPos) + return; int stepDir = (currentPos > previousPos) ? 1 : -1; int stepPos = previousPos + stepDir; while (true) { play_pet_travel_step_sound(stepPos); - if (stepPos == currentPos) break; + if (stepPos == currentPos) + break; stepPos += stepDir; } petPosition = currentPos; @@ -280,7 +299,8 @@ bool is_pet_knocked_out() { return petKnockoutHoursRemaining > 0 || petHealth <= 0; } -void begin_pet_adventure(PetAdventureFindTargetCallback@ findTarget, PetAdventureDamageCallback@ damageTarget, int playerPos) { +void begin_pet_adventure(PetAdventureFindTargetCallback @findTarget, PetAdventureDamageCallback @damageTarget, + int playerPos) { petAdventureMode = true; petAdventurePlayerPos = playerPos; petAdventurePreOut = petOut; @@ -320,8 +340,11 @@ void end_pet_adventure() { } void queue_pet_return_event() { - if (!petActive) return; - queue_pet_event("A " + get_pet_display_name() + " returns to you."); + if (!petActive) + return; + dictionary returnArgs; + returnArgs.set("pet", get_pet_display_name()); + queue_pet_event(trf("system.pet.returned", returnArgs)); } bool is_pet_at_player_position() { @@ -329,12 +352,14 @@ bool is_pet_at_player_position() { } bool is_pet_within_follow_distance() { - if (!petPositionValid) return false; + if (!petPositionValid) + return false; return abs(petPosition - get_pet_listener_pos()) <= PET_FOLLOW_MAX_DISTANCE; } bool is_pet_within_follow_comfort_distance() { - if (!petPositionValid) return false; + if (!petPositionValid) + return false; return abs(petPosition - get_pet_listener_pos()) <= PET_FOLLOW_COMFORT_DISTANCE; } @@ -347,13 +372,16 @@ void complete_pet_return() { } void knock_out_pet() { - if (!petActive) return; + if (!petActive) + return; petHealth = 0; petKnockoutHoursRemaining = PET_KNOCKOUT_COOLDOWN_HOURS; petOut = false; petPositionValid = false; stop_pet_travel(); - queue_pet_event("Your " + get_pet_display_name() + " has been knocked out."); + dictionary knockedOutArgs; + knockedOutArgs.set("pet", get_pet_display_name()); + queue_pet_event(trf("system.pet.knocked_out", knockedOutArgs)); } void update_pet_events() { @@ -377,9 +405,10 @@ void update_pet_events() { string soundPath = petEventSounds[0]; int soundPos = petEventPositions[0]; - if (soundPath != "" && file_exists(soundPath)) { + if (soundPath != "" && audio_asset_exists(soundPath)) { if (soundPos >= 0) { - petEventSoundHandle = play_1d_with_volume_step(soundPath, get_pet_listener_pos(), soundPos, false, PLAYER_WEAPON_SOUND_VOLUME_STEP); + petEventSoundHandle = play_1d_with_volume_step(soundPath, get_pet_listener_pos(), soundPos, false, + PLAYER_WEAPON_SOUND_VOLUME_STEP); } else { petEventSoundHandle = p.play_stationary(soundPath, false); } @@ -432,11 +461,15 @@ void reset_pet_state() { } void pet_leave() { - if (!petActive) return; + if (!petActive) + return; string oldPet = normalize_pet_name_text(petType); - if (oldPet == "") oldPet = "pet"; - speak_with_history(oldPet + " leaves.", true); + if (oldPet == "") + oldPet = tr("system.pet.default_name"); + dictionary leaveArgs; + leaveArgs.set("pet", oldPet); + speak_with_history(trf("system.pet.leaves", leaveArgs), true); if (!petOut) { reset_pet_state(); @@ -449,6 +482,30 @@ void pet_leave() { stop_pet_travel(); } +bool abandon_pet() { + if (!petActive) + return false; + + if (is_pet_knocked_out()) { + dictionary unconsciousArgs; + unconsciousArgs.set("pet", get_pet_display_name()); + speak_with_history(trf("system.character.pet.unconscious_cannot_abandon", unconsciousArgs), true); + return false; + } + + // Force the pet out first so leaving uses travel-out behavior instead of instant reset. + if (!petOut) { + petOut = true; + petRecallRequested = false; + petPosition = get_pet_listener_pos(); + petPositionValid = true; + } + + petLoyalty = 0; + pet_leave(); + return true; +} + void start_pet_travel_leave() { stop_pet_travel(); petTravelActive = true; @@ -469,24 +526,22 @@ void start_pet_travel_leave() { petTravelDurationMs = get_pet_travel_duration_ms(petTravelStartPos, petTravelTargetPos, true); petTravelTimer.restart(); - if (petSoundPath != "" && file_exists(petSoundPath)) { - petTravelSoundHandle = play_1d_with_volume_step( - petSoundPath, - get_pet_listener_pos(), - petTravelStartPos, - true, - PLAYER_WEAPON_SOUND_VOLUME_STEP - ); + if (petSoundPath != "" && audio_asset_exists(petSoundPath)) { + petTravelSoundHandle = play_1d_with_volume_step(petSoundPath, get_pet_listener_pos(), petTravelStartPos, true, + PLAYER_WEAPON_SOUND_VOLUME_STEP); } } void clamp_pet_loyalty() { - if (petLoyalty < 0) petLoyalty = 0; - if (petLoyalty > PET_LOYALTY_MAX) petLoyalty = PET_LOYALTY_MAX; + if (petLoyalty < 0) + petLoyalty = 0; + if (petLoyalty > PET_LOYALTY_MAX) + petLoyalty = PET_LOYALTY_MAX; } void adjust_pet_loyalty(int delta) { - if (!petActive) return; + if (!petActive) + return; petLoyalty += delta; clamp_pet_loyalty(); if (petLoyalty <= 0) { @@ -495,22 +550,31 @@ void adjust_pet_loyalty(int delta) { } void check_pet_call_key() { - if (!key_pressed(KEY_SPACE)) return; + if (!key_pressed(KEY_SPACE)) + return; if (petLeaving) { string name = petLeavingName; - if (name == "") name = "pet"; - speak_with_history("Your " + name + " is leaving.", true); + if (name == "") + name = tr("system.pet.default_name"); + dictionary leavingArgs; + leavingArgs.set("pet", name); + speak_with_history(trf("system.pet.leaving", leavingArgs), true); return; } if (!petActive) { - speak_with_history("No pet.", true); + speak_with_history(tr("system.character.pet.no_pet"), true); return; } if (is_pet_knocked_out()) { - string message = "Your " + get_pet_display_name() + " has been knocked out."; + dictionary messageArgs; + messageArgs.set("pet", get_pet_display_name()); + string message = trf("system.pet.knocked_out", messageArgs); if (petKnockoutHoursRemaining > 0) { - string hourLabel = (petKnockoutHoursRemaining == 1) ? "hour" : "hours"; - message += " " + petKnockoutHoursRemaining + " " + hourLabel + " remaining."; + string hourLabel = (petKnockoutHoursRemaining == 1) ? tr("system.character.word.hour") + : tr("system.character.word.hours"); + messageArgs.set("hours", petKnockoutHoursRemaining); + messageArgs.set("hour_label", hourLabel); + message = trf("system.pet.knocked_out_with_remaining", messageArgs); } speak_with_history(message, true); return; @@ -518,28 +582,31 @@ void check_pet_call_key() { if (petOut) { petRecallRequested = true; play_pet_recall_ack(); - speak_with_history("Your pet is on its way.", true); + speak_with_history(tr("system.pet.on_the_way"), true); request_pet_return(); return; } if (petLoyalty <= 2) { - speak_with_history("A " + get_pet_display_name() + " is hungry and unresponsive.", true); + dictionary hungryArgs; + hungryArgs.set("pet", get_pet_display_name()); + speak_with_history(trf("system.pet.hungry_unresponsive", hungryArgs), true); return; } adjust_pet_loyalty(-PET_LOYALTY_CALLOUT_COST); - if (!petActive) return; + if (!petActive) + return; petOut = true; petRecallRequested = false; petPosition = get_pet_listener_pos(); petPositionValid = true; - if (file_exists("sounds/actions/call_pet.ogg")) { + if (audio_asset_exists("sounds/actions/call_pet.ogg")) { /* But I can feel it, black water washes over me. As it soothes I call to you with my control. */ p.play_stationary("sounds/actions/call_pet.ogg", false); } } -void adopt_pet(const string&in soundPath) { +void adopt_pet(const string& in soundPath) { petActive = true; petSoundPath = soundPath; petType = get_pet_name_from_sound_path(soundPath); @@ -555,7 +622,9 @@ void adopt_pet(const string&in soundPath) { petAttackTimer.restart(); petRetrieveTimer.restart(); petTravelTimer.restart(); - speak_with_history("A " + get_pet_display_name() + " joins you.", true); + dictionary joinsArgs; + joinsArgs.set("pet", get_pet_display_name()); + speak_with_history(trf("system.pet.joins_you", joinsArgs), true); } void stop_pet_travel() { @@ -575,15 +644,17 @@ int get_pet_travel_duration_ms(int startPos, int targetPos, bool chargeTravel = int speedPerTile = walk_speed; if (chargeTravel) { speedPerTile = int(float(walk_speed) / PET_CHARGE_SPEED_MULTIPLIER); - if (speedPerTile < 1) speedPerTile = 1; + if (speedPerTile < 1) + speedPerTile = 1; } int duration = distance * speedPerTile; - if (duration < PET_TRAVEL_MIN_MS) duration = PET_TRAVEL_MIN_MS; + if (duration < PET_TRAVEL_MIN_MS) + duration = PET_TRAVEL_MIN_MS; return duration; } -void start_pet_travel_attack(int targetPos, const string&in targetLabel, int targetKind) { +void start_pet_travel_attack(int targetPos, const string& in targetLabel, int targetKind) { stop_pet_travel(); petTravelActive = true; petTravelAction = PET_TRAVEL_ATTACK; @@ -596,14 +667,9 @@ void start_pet_travel_attack(int targetPos, const string&in targetLabel, int tar petTravelDurationMs = get_pet_travel_duration_ms(petTravelStartPos, targetPos, true); petTravelTimer.restart(); - if (petSoundPath != "" && file_exists(petSoundPath)) { - petTravelSoundHandle = play_1d_with_volume_step( - petSoundPath, - get_pet_listener_pos(), - petTravelStartPos, - true, - PLAYER_WEAPON_SOUND_VOLUME_STEP - ); + if (petSoundPath != "" && audio_asset_exists(petSoundPath)) { + petTravelSoundHandle = play_1d_with_volume_step(petSoundPath, get_pet_listener_pos(), petTravelStartPos, true, + PLAYER_WEAPON_SOUND_VOLUME_STEP); } } @@ -632,8 +698,10 @@ void start_pet_travel_return(int targetPos) { } void request_pet_return() { - if (petLeaving) return; - if (!petActive || !petOut) return; + if (petLeaving) + return; + if (!petActive || !petOut) + return; if (!petPositionValid) { petPosition = get_pet_listener_pos(); petPositionValid = true; @@ -646,8 +714,10 @@ void request_pet_return() { } void request_pet_follow() { - if (petLeaving) return; - if (!petActive || !petOut) return; + if (petLeaving) + return; + if (!petActive || !petOut) + return; petRecallRequested = false; if (!petPositionValid) { petPosition = get_pet_listener_pos(); @@ -661,8 +731,10 @@ void request_pet_follow() { } void play_pet_recall_ack() { - if (!petActive) return; - if (petSoundPath == "" || !file_exists(petSoundPath)) return; + if (!petActive) + return; + if (petSoundPath == "" || !audio_asset_exists(petSoundPath)) + return; int listenerPos = get_pet_listener_pos(); if (petPositionValid) { play_1d_with_volume_step(petSoundPath, listenerPos, petPosition, false, PLAYER_WEAPON_SOUND_VOLUME_STEP); @@ -672,8 +744,10 @@ void play_pet_recall_ack() { } void update_pet_travel() { - if (!petTravelActive) return; - if (petTravelDurationMs < 1) petTravelDurationMs = 1; + if (!petTravelActive) + return; + if (petTravelDurationMs < 1) + petTravelDurationMs = 1; if (petTravelAction == PET_TRAVEL_RETURN) { if (!petPositionValid) { @@ -697,9 +771,11 @@ void update_pet_travel() { int stepInterval = walk_speed; if (distanceToPlayer > PET_FOLLOW_MAX_DISTANCE) { stepInterval = int(float(walk_speed) / PET_CHARGE_SPEED_MULTIPLIER); - if (stepInterval < 1) stepInterval = 1; + if (stepInterval < 1) + stepInterval = 1; } - if (stepInterval < PET_TRAVEL_MIN_MS) stepInterval = PET_TRAVEL_MIN_MS; + if (stepInterval < PET_TRAVEL_MIN_MS) + stepInterval = PET_TRAVEL_MIN_MS; if (petTravelTimer.elapsed < stepInterval) { return; @@ -734,7 +810,8 @@ void update_pet_travel() { int elapsed = petTravelTimer.elapsed; float progress = float(elapsed) / float(petTravelDurationMs); - if (progress > 1.0f) progress = 1.0f; + if (progress > 1.0f) + progress = 1.0f; int travel = int(float(petTravelTargetPos - petTravelStartPos) * progress); int currentPos = petTravelStartPos + travel; @@ -743,7 +820,14 @@ void update_pet_travel() { p.update_sound_1d(petTravelSoundHandle, currentPos); } - if (elapsed < petTravelDurationMs) return; + if (petTravelAction == PET_TRAVEL_LEAVE && + abs(currentPos - get_pet_listener_pos()) >= PET_LEAVE_DESPAWN_DISTANCE) { + reset_pet_state(); + return; + } + + if (elapsed < petTravelDurationMs) + return; safe_destroy_sound(petTravelSoundHandle); petTravelSoundHandle = -1; @@ -776,7 +860,7 @@ void update_pet_travel() { int nextTargetKind = -1; bool hasNextTarget = find_pet_attack_target(nextTargetPos, nextTargetLabel, nextTargetKind); if (!hasNextTarget) { - WorldDrop@ drop = find_pet_drop_target(); + WorldDrop @drop = find_pet_drop_target(); if (drop !is null) { petRetrieveTimer.restart(); start_pet_travel_retrieve(drop.position); @@ -785,7 +869,7 @@ void update_pet_travel() { request_pet_follow(); } } else if (petTravelAction == PET_TRAVEL_RETRIEVE) { - WorldDrop@ drop = get_drop_at(petTravelTargetPos); + WorldDrop @drop = get_drop_at(petTravelTargetPos); if (drop !is null) { string message = ""; if (try_pet_pickup_world_drop(drop, message)) { @@ -793,7 +877,7 @@ void update_pet_travel() { queue_pet_event(message, -1, false); petPosition = petTravelTargetPos; petPositionValid = true; - WorldDrop@ nextDrop = find_pet_drop_target(); + WorldDrop @nextDrop = find_pet_drop_target(); if (nextDrop !is null) { petRetrieveTimer.restart(); start_pet_travel_retrieve(nextDrop.position); @@ -818,20 +902,25 @@ void update_pet_travel() { stop_pet_travel(); } -bool run_pet_offer_menu(const string&in soundPath, const string&in reasonText) { - if (petActive) return false; - if (soundPath == "" || !file_exists(soundPath)) return false; - if (!has_pet_food_available()) return false; +bool run_pet_offer_menu(const string& in soundPath, const string& in reasonText) { + if (petActive) + return false; + if (soundPath == "" || !audio_asset_exists(soundPath)) + return false; + if (!has_pet_food_available()) + return false; string petName = get_pet_name_from_sound_path(soundPath); - string prompt = "A friendly looking " + petName + " begs for food. Accept?"; + dictionary promptArgs; + promptArgs.set("pet", petName); + string prompt = trf("system.pet.offer.prompt", promptArgs); if (reasonText != "") { prompt += " " + reasonText; } string[] options; - options.insert_last("Yes"); - options.insert_last("No"); + options.insert_last(tr("system.option.yes")); + options.insert_last(tr("system.option.no")); int selection = 0; speak_with_history(prompt + " " + options[selection], true); @@ -842,19 +931,21 @@ bool run_pet_offer_menu(const string&in soundPath, const string&in reasonText) { return false; } if (key_pressed(KEY_ESCAPE)) { - speak_with_history("Declined.", true); + speak_with_history(tr("system.pet.offer.declined"), true); return false; } if (key_pressed(KEY_DOWN)) { play_menu_move_sound(); selection++; - if (selection >= int(options.length())) selection = 0; + if (selection >= int(options.length())) + selection = 0; speak_with_history(options[selection], true); } if (key_pressed(KEY_UP)) { play_menu_move_sound(); selection--; - if (selection < 0) selection = int(options.length()) - 1; + if (selection < 0) + selection = int(options.length()) - 1; speak_with_history(options[selection], true); } if (key_pressed(KEY_RETURN)) { @@ -863,90 +954,111 @@ bool run_pet_offer_menu(const string&in soundPath, const string&in reasonText) { adopt_pet(soundPath); return true; } - speak_with_history("Declined.", true); + speak_with_history(tr("system.pet.offer.declined"), true); return false; } } return false; } -void attempt_pet_offer_random(const string&in reasonText) { - if (petActive) return; - if (!has_pet_food_available()) return; +void attempt_pet_offer_random(const string& in reasonText) { + if (petActive) + return; + if (!has_pet_food_available()) + return; init_pet_sounds(); - if (petSoundPaths.length() == 0) return; + if (petSoundPaths.length() == 0) + return; int pick = random(0, petSoundPaths.length() - 1); run_pet_offer_menu(petSoundPaths[pick], reasonText); } void attempt_pet_offer_from_quest(int score) { - if (score < QUEST_LOG_SCORE) return; + if (score < QUEST_LOG_SCORE) + return; attempt_pet_offer_random(""); } void attempt_pet_offer_from_adventure() { - if (petActive) return; - if (random(1, 100) > PET_ADVENTURE_CHANCE) return; + if (petActive) + return; + if (random(1, 100) > PET_ADVENTURE_CHANCE) + return; attempt_pet_offer_random(""); } void attempt_pet_offer_from_tree() { - if (petActive) return; - if (random(1, 100) > PET_TREE_HAWK_CHANCE) return; + if (petActive) + return; + if (random(1, 100) > PET_TREE_HAWK_CHANCE) + return; string hawkSound = get_pet_sound_for_name("hawk"); - if (hawkSound == "") return; + if (hawkSound == "") + return; run_pet_offer_menu(hawkSound, ""); } -bool try_pet_pickup_small_game(const string&in gameType, string &out message) { +bool try_pet_pickup_small_game(const string& in gameType, string& out message) { + string localizedGameType = i18n_translate_fragment_value(gameType); + dictionary pickupArgs; + pickupArgs.set("item", localizedGameType); + pickupArgs.set("pet", get_pet_display_name()); if (get_personal_count(ITEM_SMALL_GAME) >= get_personal_stack_limit()) { - message = "You can't carry a " + gameType + ", so you give it to a " + get_pet_display_name() + " to chew on."; + message = trf("system.pet.retrieve.cant_carry_chew_on", pickupArgs); return true; } add_personal_count(ITEM_SMALL_GAME, 1); personal_small_game_types.insert_last(gameType); - message = "Your " + get_pet_display_name() + " retrieved " + gameType + "."; + message = trf("system.pet.retrieve.retrieved_item", pickupArgs); return true; } -bool try_pet_pickup_world_drop(WorldDrop@ drop, string &out message) { - if (drop is null) return false; +bool try_pet_pickup_world_drop(WorldDrop @drop, string& out message) { + if (drop is null) + return false; if (get_flying_creature_config_by_drop_type(drop.type) !is null) { return try_pet_pickup_small_game(drop.type, message); } if (drop.type == "arrow") { + dictionary arrowArgs; + arrowArgs.set("item", i18n_translate_fragment_value("arrow")); + arrowArgs.set("pet", get_pet_display_name()); int maxArrows = get_arrow_limit(); if (maxArrows <= 0) { - message = "You can't carry an arrow, so you give it to a " + get_pet_display_name() + " to chew on."; + message = trf("system.pet.retrieve.cant_carry_chew_on", arrowArgs); return true; } if (get_personal_count(ITEM_ARROWS) >= maxArrows) { - message = "You can't carry an arrow, so you give it to a " + get_pet_display_name() + " to chew on."; + message = trf("system.pet.retrieve.cant_carry_chew_on", arrowArgs); return true; } add_personal_count(ITEM_ARROWS, 1); - message = "Your " + get_pet_display_name() + " retrieved an arrow."; + message = trf("system.pet.retrieve.retrieved_item", arrowArgs); return true; } if (drop.type == "boar carcass") { + dictionary boarArgs; + boarArgs.set("item", i18n_translate_fragment_value("boar carcass")); + boarArgs.set("pet", get_pet_display_name()); if (get_personal_count(ITEM_BOAR_CARCASSES) >= get_personal_stack_limit()) { - message = "You can't carry a boar carcass, so you give it to a " + get_pet_display_name() + " to chew on."; + message = trf("system.pet.retrieve.cant_carry_chew_on", boarArgs); return true; } add_personal_count(ITEM_BOAR_CARCASSES, 1); - message = "Your " + get_pet_display_name() + " retrieved a boar carcass."; + message = trf("system.pet.retrieve.retrieved_item", boarArgs); return true; } return false; } -WorldDrop@ find_pet_drop_target() { +WorldDrop @find_pet_drop_target() { int bestDistance = PET_RANGE + 1; - WorldDrop@ best = null; + WorldDrop @best = null; int origin = get_pet_search_origin(); for (uint i = 0; i < world_drops.length(); i++) { int distance = abs(world_drops[i].position - origin); - if (distance > PET_RANGE) continue; + if (distance > PET_RANGE) + continue; if (distance < bestDistance) { bestDistance = distance; @best = world_drops[i]; @@ -956,23 +1068,32 @@ WorldDrop@ find_pet_drop_target() { } void update_pet_retrieval() { - if (!petActive) return; - if (!petOut) return; - if (petLoyalty <= 0) return; - if (petAdventureMode) return; - if (is_pet_knocked_out()) return; - if (petRetrieveTimer.elapsed < PET_RETRIEVE_COOLDOWN) return; - if (petEventMessages.length() > 2) return; - if (petTravelActive) return; + if (!petActive) + return; + if (!petOut) + return; + if (petLoyalty <= 0) + return; + if (petAdventureMode) + return; + if (is_pet_knocked_out()) + return; + if (petRetrieveTimer.elapsed < PET_RETRIEVE_COOLDOWN) + return; + if (petEventMessages.length() > 2) + return; + if (petTravelActive) + return; - WorldDrop@ drop = find_pet_drop_target(); - if (drop is null) return; + WorldDrop @drop = find_pet_drop_target(); + if (drop is null) + return; petRetrieveTimer.restart(); start_pet_travel_retrieve(drop.position); } -bool find_pet_attack_target(int &out targetPos, string &out targetLabel, int &out targetKind) { +bool find_pet_attack_target(int& out targetPos, string& out targetLabel, int& out targetKind) { int bestDistance = PET_RANGE + 1; targetPos = -1; targetLabel = ""; @@ -984,7 +1105,8 @@ bool find_pet_attack_target(int &out targetPos, string &out targetLabel, int &ou for (uint i = 0; i < bandits.length(); i++) { int distance = abs(bandits[i].position - origin); - if (distance > PET_RANGE) continue; + if (distance > PET_RANGE) + continue; if (distance < bestDistance) { bestDistance = distance; targetPos = bandits[i].position; @@ -994,9 +1116,11 @@ bool find_pet_attack_target(int &out targetPos, string &out targetLabel, int &ou } for (uint i = 0; i < undeads.length(); i++) { - if (undeads[i].undead_type == "undead_resident") continue; + if (undeads[i].undead_type == "undead_resident") + continue; int distance = abs(undeads[i].position - origin); - if (distance > PET_RANGE) continue; + if (distance > PET_RANGE) + continue; if (distance < bestDistance) { bestDistance = distance; targetPos = undeads[i].position; @@ -1007,7 +1131,8 @@ bool find_pet_attack_target(int &out targetPos, string &out targetLabel, int &ou for (uint i = 0; i < ground_games.length(); i++) { int distance = abs(ground_games[i].position - origin); - if (distance > PET_RANGE) continue; + if (distance > PET_RANGE) + continue; if (distance < bestDistance) { bestDistance = distance; targetPos = ground_games[i].position; @@ -1019,7 +1144,7 @@ bool find_pet_attack_target(int &out targetPos, string &out targetLabel, int &ou return targetPos != -1; } -bool find_pet_attack_target_by_kind(int targetKind, int referencePos, int &out targetPos) { +bool find_pet_attack_target_by_kind(int targetKind, int referencePos, int& out targetPos) { int bestDistance = PET_RANGE + 1; targetPos = -1; int origin = get_pet_search_origin(); @@ -1032,7 +1157,8 @@ bool find_pet_attack_target_by_kind(int targetKind, int referencePos, int &out t if (targetKind == 0) { for (uint i = 0; i < bandits.length(); i++) { int distanceToOrigin = abs(bandits[i].position - origin); - if (distanceToOrigin > PET_RANGE) continue; + if (distanceToOrigin > PET_RANGE) + continue; int distance = abs(bandits[i].position - referencePos); if (distance < bestDistance) { bestDistance = distance; @@ -1041,9 +1167,11 @@ bool find_pet_attack_target_by_kind(int targetKind, int referencePos, int &out t } } else if (targetKind == 1) { for (uint i = 0; i < undeads.length(); i++) { - if (undeads[i].undead_type == "undead_resident") continue; + if (undeads[i].undead_type == "undead_resident") + continue; int distanceToOrigin = abs(undeads[i].position - origin); - if (distanceToOrigin > PET_RANGE) continue; + if (distanceToOrigin > PET_RANGE) + continue; int distance = abs(undeads[i].position - referencePos); if (distance < bestDistance) { bestDistance = distance; @@ -1053,7 +1181,8 @@ bool find_pet_attack_target_by_kind(int targetKind, int referencePos, int &out t } else if (targetKind == 2) { for (uint i = 0; i < ground_games.length(); i++) { int distanceToOrigin = abs(ground_games[i].position - origin); - if (distanceToOrigin > PET_RANGE) continue; + if (distanceToOrigin > PET_RANGE) + continue; int distance = abs(ground_games[i].position - referencePos); if (distance < bestDistance) { bestDistance = distance; @@ -1066,81 +1195,113 @@ bool find_pet_attack_target_by_kind(int targetKind, int referencePos, int &out t } void update_pet_attack() { - if (!petActive) return; - if (!petOut) return; - if (petLoyalty <= 0) return; - if (is_pet_knocked_out()) return; - if (petAttackTimer.elapsed < PET_ATTACK_COOLDOWN) return; - if (petTravelActive) return; + if (!petActive) + return; + if (!petOut) + return; + if (petLoyalty <= 0) + return; + if (is_pet_knocked_out()) + return; + if (petAttackTimer.elapsed < PET_ATTACK_COOLDOWN) + return; + if (petTravelActive) + return; int targetPos = -1; string targetLabel = ""; int targetKind = -1; - if (!find_pet_attack_target(targetPos, targetLabel, targetKind)) return; + if (!find_pet_attack_target(targetPos, targetLabel, targetKind)) + return; petAttackTimer.restart(); start_pet_travel_attack(targetPos, targetLabel, targetKind); } void update_pet_follow() { - if (!petActive) return; - if (!petOut) return; - if (petAdventureMode) return; - if (is_pet_knocked_out()) return; - if (petTravelActive) return; + if (!petActive) + return; + if (!petOut) + return; + if (petAdventureMode) + return; + if (is_pet_knocked_out()) + return; + if (petTravelActive) + return; - WorldDrop@ drop = find_pet_drop_target(); - if (drop !is null) return; + WorldDrop @drop = find_pet_drop_target(); + if (drop !is null) + return; int targetPos = -1; string targetLabel = ""; int targetKind = -1; - if (find_pet_attack_target(targetPos, targetLabel, targetKind)) return; + if (find_pet_attack_target(targetPos, targetLabel, targetKind)) + return; - if (is_pet_within_follow_comfort_distance()) return; + if (is_pet_within_follow_comfort_distance()) + return; request_pet_follow(); } void attempt_pet_random_find() { - if (!petActive) return; - if (!petOut) return; - if (petLoyalty < PET_LOYALTY_BONUS_THRESHOLD) return; - if (random(1, 100) > PET_RANDOM_FIND_CHANCE) return; + if (!petActive) + return; + if (!petOut) + return; + if (petLoyalty < PET_LOYALTY_BONUS_THRESHOLD) + return; + if (random(1, 100) > PET_RANDOM_FIND_CHANCE) + return; int[] possibleItems = {ITEM_STICKS, ITEM_VINES, ITEM_STONES, ITEM_CLAY}; int itemType = possibleItems[random(0, possibleItems.length() - 1)]; int added = add_to_stack(get_personal_count(itemType), 1); - if (added <= 0) return; + if (added <= 0) + return; add_personal_count(itemType, added); string itemName = (added == 1) ? item_registry[itemType].singular : item_registry[itemType].name; - queue_pet_event("Your " + get_pet_display_name() + " retrieved " + added + " " + itemName + ".", petPositionValid ? petPosition : x, false); + dictionary randomFindArgs; + randomFindArgs.set("pet", get_pet_display_name()); + randomFindArgs.set("count", added); + randomFindArgs.set("item", i18n_translate_fragment_value(itemName)); + queue_pet_event(trf("system.pet.retrieve.random_find", randomFindArgs), petPositionValid ? petPosition : x, false); request_pet_follow(); } void handle_pet_hourly_update(int hour) { - if (!petActive) return; - if (petLeaving) return; + if (!petActive) + return; + if (petLeaving) + return; if (petKnockoutHoursRemaining > 0) { petKnockoutHoursRemaining--; if (petKnockoutHoursRemaining <= 0) { petKnockoutHoursRemaining = 0; petHealth = PET_HEALTH_MAX; - notify("Your " + get_pet_display_name() + " has recovered from its injuries."); + dictionary recoveredArgs; + recoveredArgs.set("pet", get_pet_display_name()); + notify(trf("system.pet.recovered_from_injuries", recoveredArgs)); } } if (petKnockoutHoursRemaining <= 0 && !petOut && petHealth > 0 && petHealth < PET_HEALTH_MAX) { petHealth += 1; - if (petHealth > PET_HEALTH_MAX) petHealth = PET_HEALTH_MAX; + if (petHealth > PET_HEALTH_MAX) + petHealth = PET_HEALTH_MAX; } if (petOut) { adjust_pet_loyalty(-1); - if (!petActive || petLeaving) return; + if (!petActive || petLeaving) + return; if (petLoyalty <= 1 && !petHungryWarned) { - notify("A " + get_pet_display_name() + " is getting hungry."); + dictionary hungryArgs; + hungryArgs.set("pet", get_pet_display_name()); + notify(trf("system.pet.getting_hungry", hungryArgs)); petHungryWarned = true; } } diff --git a/src/player.nvgt b/src/player.nvgt index 1a61547..b553394 100644 --- a/src/player.nvgt +++ b/src/player.nvgt @@ -7,11 +7,11 @@ int player_sex = SEX_MALE; bool jumping = false; bool climbing = false; bool falling = false; -int climb_target_y = 0; // Target height when climbing -int fall_start_y = 0; // Height where fall started +int climb_target_y = 0; // Target height when climbing +int fall_start_y = 0; // Height where fall started int fall_sound_handle = -1; // Handle for looping fall sound -timer fall_timer; // For fall sound pitch -timer climb_timer; // For climb speed +timer fall_timer; // For fall sound pitch +timer climb_timer; // For climb speed // Rope climbing state bool rope_climbing = false; @@ -52,25 +52,25 @@ bool bow_shot_drop_pending = false; int bow_shot_drop_pos = -1; // Fishing state -bool is_casting = false; // Holding control, cast_strength playing -bool line_in_water = false; // Cast successful, waiting for fish -bool fish_on_line = false; // Fish caught, reeling phase -bool is_reeling = false; // Holding control during reel -int cast_position = -1; // Current position of cast cursor -int reel_position = -1; // Position of fish during reel -int line_position = -1; // Where the line landed in the water -int cast_origin_x = -1; // Player position when casting started -int cast_direction = 0; // 1 = moving toward stream, -1 = moving back -int reel_direction = 0; // 1 = moving toward player, -1 = moving past -int reel_start_direction = 0; // Initial reel direction toward player -int target_stream_start = -1; // Start of target stream for casting -int target_stream_end = -1; // End of target stream for casting -timer fishing_timer; // Tracks time line has been in water -timer cast_move_timer; // Controls discrete position movement (150ms per tile) -int catch_chance = 0; // Current catch percentage (starts 5% after 1s, +1% per second, max 95%) -int cast_sound_handle = -1; // Handle for cast_strength.ogg -string hooked_fish_type = ""; // Type of fish on the line -int fishing_checks_done = 0; // Number of catch checks performed while waiting +bool is_casting = false; // Holding control, cast_strength playing +bool line_in_water = false; // Cast successful, waiting for fish +bool fish_on_line = false; // Fish caught, reeling phase +bool is_reeling = false; // Holding control during reel +int cast_position = -1; // Current position of cast cursor +int reel_position = -1; // Position of fish during reel +int line_position = -1; // Where the line landed in the water +int cast_origin_x = -1; // Player position when casting started +int cast_direction = 0; // 1 = moving toward stream, -1 = moving back +int reel_direction = 0; // 1 = moving toward player, -1 = moving past +int reel_start_direction = 0; // Initial reel direction toward player +int target_stream_start = -1; // Start of target stream for casting +int target_stream_end = -1; // End of target stream for casting +timer fishing_timer; // Tracks time line has been in water +timer cast_move_timer; // Controls discrete position movement (150ms per tile) +int catch_chance = 0; // Current catch percentage (starts 5% after 1s, +1% per second, max 95%) +int cast_sound_handle = -1; // Handle for cast_strength.ogg +string hooked_fish_type = ""; // Type of fish on the line +int fishing_checks_done = 0; // Number of catch checks performed while waiting // Favor system double favor = 0.0; diff --git a/src/quest_system.nvgt b/src/quest_system.nvgt index 0f02626..5000412 100644 --- a/src/quest_system.nvgt +++ b/src/quest_system.nvgt @@ -16,37 +16,44 @@ int[] quest_queue; bool quest_roll_done_today = false; string get_quest_name(int quest_type) { - if (quest_type == QUEST_BAT_INVASION) return "Bat Invasion"; - if (quest_type == QUEST_CATCH_BOOMERANG) return "Catch the Boomerang"; - if (quest_type == QUEST_ENCHANTED_MELODY) return "Enchanted Melody"; - if (quest_type == QUEST_ESCAPE_FROM_HEL) return "Escape from Hel"; - if (quest_type == QUEST_SKELETAL_BARD) return "Skeletal Bard"; - return "Unknown Quest"; + if (quest_type == QUEST_BAT_INVASION) + return tr("system.quest.name.bat_invasion"); + if (quest_type == QUEST_CATCH_BOOMERANG) + return tr("system.quest.name.catch_boomerang"); + if (quest_type == QUEST_ENCHANTED_MELODY) + return tr("system.quest.name.enchanted_melody"); + if (quest_type == QUEST_ESCAPE_FROM_HEL) + return tr("system.quest.name.escape_from_hel"); + if (quest_type == QUEST_SKELETAL_BARD) + return tr("system.quest.name.skeletal_bard"); + return tr("system.quest.name.unknown"); } string get_quest_description(int quest_type) { if (quest_type == QUEST_BAT_INVASION) { - return "Bat Invasion. Giant killer bats are attacking. Press space to throw when the bat is centered."; + return tr("system.quest.description.bat_invasion"); } if (quest_type == QUEST_CATCH_BOOMERANG) { - return "Catch the Boomerang. Throw and catch the boomerang as it returns for up to 4 points."; + return tr("system.quest.description.catch_boomerang"); } if (quest_type == QUEST_ENCHANTED_MELODY) { - return "Enchanted Melody. Repeat the pattern using E R D F or U I J K. Lowest to highest pitch."; + return tr("system.quest.description.enchanted_melody"); } if (quest_type == QUEST_ESCAPE_FROM_HEL) { - return "Escape from Hel. Press space to jump over open graves. The pace quickens."; + return tr("system.quest.description.escape_from_hel"); } if (quest_type == QUEST_SKELETAL_BARD) { - return "Skeletal Bard. A skeleton named Billy Bones is practicing to become a bard. Count the notes."; + return tr("system.quest.description.skeletal_bard"); } - return "Unknown quest."; + return tr("system.quest.description.unknown"); } int get_quest_chance_from_favor() { int chance = int(favor * QUEST_CHANCE_PER_FAVOR); - if (chance < QUEST_MIN_CHANCE && favor >= 1.0) chance = QUEST_MIN_CHANCE; - if (chance > 100) chance = 100; + if (chance < QUEST_MIN_CHANCE && favor >= 1.0) + chance = QUEST_MIN_CHANCE; + if (chance > 100) + chance = 100; return chance; } @@ -59,21 +66,29 @@ void quest_boomerang_hit_sound() { } void add_quest(int quest_type) { - if (quest_queue.length() >= QUEST_MAX_ACTIVE) return; + if (quest_queue.length() >= QUEST_MAX_ACTIVE) + return; quest_queue.insert_last(quest_type); - notify("A new quest is available: " + get_quest_name(quest_type) + "."); + dictionary newQuestArgs; + newQuestArgs.set("quest", get_quest_name(quest_type)); + notify(trf("system.quest.new_available", newQuestArgs)); } void attempt_daily_quest() { - if (quest_roll_done_today) return; - if (favor < 1.0) return; - if (world_altars.length() == 0) return; - if (quest_queue.length() >= QUEST_MAX_ACTIVE) return; + if (quest_roll_done_today) + return; + if (favor < 1.0) + return; + if (world_altars.length() == 0) + return; + if (quest_queue.length() >= QUEST_MAX_ACTIVE) + return; quest_roll_done_today = true; int chance = get_quest_chance_from_favor(); int roll = random(1, 100); - if (roll > chance) return; + if (roll > chance) + return; int quest_type = random(0, QUEST_TYPE_COUNT - 1); add_quest(quest_type); @@ -82,11 +97,11 @@ void attempt_daily_quest() { void check_quest_menu() { if (key_pressed(KEY_Q)) { if (x > BASE_END) { - speak_with_history("You are not in the base.", true); + speak_with_history(tr("system.base_info.not_in_base"), true); return; } if (quest_queue.length() == 0) { - speak_with_history("No quests available.", true); + speak_with_history(tr("system.quest.none_available"), true); return; } run_quest_menu(); @@ -94,18 +109,24 @@ void check_quest_menu() { } string get_quest_favor_phrase(int score) { - if (score <= 2) return "The gods are displeased with your performance."; - if (score < QUEST_STONE_SCORE) return "The gods are pleased with your performance."; + if (score <= 2) + return tr("system.quest.favor_phrase.displeased"); + if (score < QUEST_STONE_SCORE) + return tr("system.quest.favor_phrase.pleased"); int extreme_threshold = QUEST_LOG_SCORE - 2; - if (score < extreme_threshold) return "The gods are very pleased with your performance."; - if (score < QUEST_LOG_SCORE) return "The gods are extremely pleased with your performance."; - return "The gods are ultimately pleased with your performance."; + if (score < extreme_threshold) + return tr("system.quest.favor_phrase.very_pleased"); + if (score < QUEST_LOG_SCORE) + return tr("system.quest.favor_phrase.extremely_pleased"); + return tr("system.quest.favor_phrase.ultimately_pleased"); } void apply_quest_reward(int score) { double favor_gain = score * QUEST_FAVOR_PER_POINT; - if (favor_gain < 0) favor_gain = 0; - if (favor_gain > 1.0) favor_gain = 1.0; + if (favor_gain < 0) + favor_gain = 0; + if (favor_gain > 1.0) + favor_gain = 1.0; favor += favor_gain; // Determine quantity based on score @@ -115,24 +136,26 @@ void apply_quest_reward(int score) { // 8-9: 3 // >= 10: 4 int quantity = 0; - if (score >= QUEST_LOG_SCORE) quantity = 4; - else if (score >= QUEST_LOG_SCORE - 2) quantity = 3; - else if (score >= QUEST_STONE_SCORE) quantity = 2; - else if (score > 2) quantity = 1; + if (score >= QUEST_LOG_SCORE) + quantity = 4; + else if (score >= QUEST_LOG_SCORE - 2) + quantity = 3; + else if (score >= QUEST_STONE_SCORE) + quantity = 2; + else if (score > 2) + quantity = 1; // Select reward item - int[] possible_rewards = { - ITEM_STICKS, ITEM_VINES, ITEM_REEDS, ITEM_STONES, ITEM_LOGS, ITEM_CLAY, - ITEM_MEAT, ITEM_SKINS, ITEM_FEATHERS, ITEM_DOWN, ITEM_FISH, ITEM_SMOKED_FISH, - ITEM_HEAL_SCROLL, ITEM_INCENSE, ITEM_BASKET_FOOD - }; + int[] possible_rewards = {ITEM_STICKS, ITEM_VINES, ITEM_REEDS, ITEM_STONES, ITEM_LOGS, + ITEM_CLAY, ITEM_MEAT, ITEM_SKINS, ITEM_FEATHERS, ITEM_DOWN, + ITEM_FISH, ITEM_SMOKED_FISH, ITEM_HEAL_SCROLL, ITEM_INCENSE, ITEM_BASKET_FOOD}; int reward_item = possible_rewards[random(0, possible_rewards.length() - 1)]; - + int added_amount = 0; if (quantity > 0) { added_amount = add_to_stack(get_personal_count(reward_item), quantity); add_personal_count(reward_item, added_amount); - + // Special handling for fish which needs weights if (reward_item == ITEM_FISH) { for (int i = 0; i < added_amount; i++) { @@ -141,31 +164,45 @@ void apply_quest_reward(int score) { } } - string message = "Quest Complete!\n\nRewards:\n"; + string message = tr("system.quest.rewards.complete_heading") + "\n\n" + tr("system.quest.rewards.heading") + "\n"; message += get_quest_favor_phrase(score) + "\n"; - message += "Favor: +" + format_favor(favor_gain) + "\n"; - + dictionary favorArgs; + favorArgs.set("favor", format_favor(favor_gain)); + message += trf("system.quest.rewards.favor_line", favorArgs) + "\n"; + if (added_amount > 0) { - message += get_item_display_name(reward_item) + ": +" + added_amount + "\n"; + dictionary itemLineArgs; + itemLineArgs.set("item", get_item_display_name(reward_item)); + itemLineArgs.set("amount", added_amount); + message += trf("system.quest.rewards.item_line", itemLineArgs) + "\n"; } else if (quantity > 0) { - message += "Inventory full, could not receive " + get_item_display_name(reward_item) + ".\n"; + dictionary inventoryFullArgs; + inventoryFullArgs.set("item", get_item_display_name(reward_item)); + message += trf("system.quest.rewards.inventory_full_line", inventoryFullArgs) + "\n"; } else { - message += "No items awarded.\n"; + message += tr("system.quest.rewards.no_items_awarded") + "\n"; } - - message += "\nScore: " + score; - text_reader(message, "Quest Rewards", true); + + dictionary scoreArgs; + scoreArgs.set("score", score); + message += "\n" + trf("system.quest.rewards.score_line", scoreArgs); + text_reader(message, tr("system.quest.rewards.title"), true); attempt_pet_offer_from_quest(score); } void run_quest(int quest_type) { p.pause_all(); int score = 0; - if (quest_type == QUEST_BAT_INVASION) score = run_bat_invasion(); - else if (quest_type == QUEST_CATCH_BOOMERANG) score = run_catch_the_boomerang(); - else if (quest_type == QUEST_ENCHANTED_MELODY) score = run_enchanted_melody(); - else if (quest_type == QUEST_ESCAPE_FROM_HEL) score = run_escape_from_hel(); - else if (quest_type == QUEST_SKELETAL_BARD) score = run_skeletal_bard(); + if (quest_type == QUEST_BAT_INVASION) + score = run_bat_invasion(); + else if (quest_type == QUEST_CATCH_BOOMERANG) + score = run_catch_the_boomerang(); + else if (quest_type == QUEST_ENCHANTED_MELODY) + score = run_enchanted_melody(); + else if (quest_type == QUEST_ESCAPE_FROM_HEL) + score = run_escape_from_hel(); + else if (quest_type == QUEST_SKELETAL_BARD) + score = run_skeletal_bard(); if (return_to_main_menu_requested) { p.resume_all(); return; @@ -175,7 +212,7 @@ void run_quest(int quest_type) { } void run_quest_menu() { - speak_with_history("Quest menu.", true); + speak_with_history(tr("system.quest.menu.prompt"), true); int selection = 0; string[] options; @@ -183,31 +220,38 @@ void run_quest_menu() { options.insert_last(get_quest_name(quest_queue[i])); } - while(true) { + while (true) { wait(5); if (menu_background_tick()) { return; } if (key_pressed(KEY_ESCAPE)) { - speak_with_history("Closed.", true); + speak_with_history(tr("system.menu.closed"), true); break; } - - string count_str = " " + (selection + 1) + " of " + options.length(); + + dictionary countArgs; + countArgs.set("index", selection + 1); + countArgs.set("total", options.length()); + string count_str = " " + trf("system.quest.menu.count", countArgs); if (key_pressed(KEY_DOWN)) { play_menu_move_sound(); selection++; - if (selection >= options.length()) selection = 0; - count_str = " " + (selection + 1) + " of " + options.length(); + if (selection >= options.length()) + selection = 0; + countArgs.set("index", selection + 1); + count_str = " " + trf("system.quest.menu.count", countArgs); speak_with_history(options[selection] + count_str, true); } if (key_pressed(KEY_UP)) { play_menu_move_sound(); selection--; - if (selection < 0) selection = options.length() - 1; - count_str = " " + (selection + 1) + " of " + options.length(); + if (selection < 0) + selection = options.length() - 1; + countArgs.set("index", selection + 1); + count_str = " " + trf("system.quest.menu.count", countArgs); speak_with_history(options[selection] + count_str, true); } diff --git a/src/quests/bat_invasion_game.nvgt b/src/quests/bat_invasion_game.nvgt index 887bae6..71897c4 100644 --- a/src/quests/bat_invasion_game.nvgt +++ b/src/quests/bat_invasion_game.nvgt @@ -9,15 +9,16 @@ 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); + speak_with_history(tr("system.quest.minigame.starting"), true); wait(500); int turns = 10; @@ -25,8 +26,8 @@ int run_bat_invasion() { // Position configuration const int BAT_START_DISTANCE = 15; // How far left/right the bat starts - const int CENTER_TOLERANCE = 2; // How close to center counts as a hit - const int LISTENER_POS = 0; // Player is at center + const int CENTER_TOLERANCE = 2; // How close to center counts as a hit + const int LISTENER_POS = 0; // Player is at center for (int i = 0; i < turns; i++) { // Pick random bat sound @@ -100,6 +101,8 @@ int run_bat_invasion() { wait(400); } - speak_with_history("Bat invasion complete. Score " + score + ".", true); + dictionary batScoreArgs; + batScoreArgs.set("score", score); + speak_with_history(trf("system.quest.bat_invasion.complete_score", batScoreArgs), true); return score; } diff --git a/src/quests/catch_the_boomerang_game.nvgt b/src/quests/catch_the_boomerang_game.nvgt index c98294e..e99a904 100644 --- a/src/quests/catch_the_boomerang_game.nvgt +++ b/src/quests/catch_the_boomerang_game.nvgt @@ -1,4 +1,14 @@ // Catch the Boomerang quest game +string get_boomerang_skill_text(int points) { + if (points <= 1) + return tr("system.quest.boomerang.skill.almost_dropped"); + if (points == 2) + return tr("system.quest.boomerang.skill.managed_to_catch"); + if (points == 3) + return tr("system.quest.boomerang.skill.caught"); + return tr("system.quest.boomerang.skill.expertly_caught"); +} + int run_catch_the_boomerang() { string[] instructions; instructions.insert_last("=== Catch the Boomerang ==="); @@ -6,13 +16,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); @@ -20,13 +31,12 @@ int run_catch_the_boomerang() { const int turnsTotal = 5; int score = 0; int caughtCount = 0; - string[] skillLevel = {"", "almost dropped", "managed to catch", "caught", "expertly caught"}; bool firstTurnPrompt = true; for (int turn = 0; turn < turnsTotal; turn++) { if (firstTurnPrompt) { - speak_with_history("Press Space to throw.", true); + speak_with_history(tr("system.quest.boomerang.press_space_to_throw"), true); firstTurnPrompt = false; } @@ -60,15 +70,27 @@ int run_catch_the_boomerang() { if (key_pressed(KEY_SPACE)) { if (returning && boomerangY > -5) { int points = 5 + boomerangY; - if (points < 1) points = 1; - if (points > 4) points = 4; + if (points < 1) + points = 1; + if (points > 4) + points = 4; score += points; caughtCount++; - string promptSuffix = (turn < turnsTotal - 1) ? " Press Space to throw." : ""; - speak_with_history("You " + skillLevel[points] + " the boomerang. " + points + " points." + promptSuffix, true); + string promptSuffix = ""; + if (turn < turnsTotal - 1) + promptSuffix = " " + tr("system.quest.boomerang.press_space_to_throw"); + dictionary skillArgs; + skillArgs.set("skill", get_boomerang_skill_text(points)); + skillArgs.set("points", points); + skillArgs.set("suffix", promptSuffix); + speak_with_history(trf("system.quest.boomerang.caught_result", skillArgs), true); } else { - string promptSuffix = (turn < turnsTotal - 1) ? " Press Space to throw." : ""; - speak_with_history("You missed the boomerang." + promptSuffix, true); + string promptSuffix = ""; + if (turn < turnsTotal - 1) + promptSuffix = " " + tr("system.quest.boomerang.press_space_to_throw"); + dictionary missedArgs; + missedArgs.set("suffix", promptSuffix); + speak_with_history(trf("system.quest.boomerang.missed", missedArgs), true); } resolved = true; } @@ -82,8 +104,12 @@ int run_catch_the_boomerang() { // Give one centered pass (y == 0) as a final catch window. if (boomerangY > 0) { - string promptSuffix = (turn < turnsTotal - 1) ? " Press Space to throw." : ""; - speak_with_history("The boomerang hit you." + promptSuffix, true); + string promptSuffix = ""; + if (turn < turnsTotal - 1) + promptSuffix = " " + tr("system.quest.boomerang.press_space_to_throw"); + dictionary hitArgs; + hitArgs.set("suffix", promptSuffix); + speak_with_history(trf("system.quest.boomerang.hit_you", hitArgs), true); quest_boomerang_hit_sound(); resolved = true; } @@ -93,6 +119,9 @@ int run_catch_the_boomerang() { wait(400); } - speak_with_history("You caught " + caughtCount + " boomerangs for a total of " + score + " points.", true); + dictionary boomerangSummaryArgs; + boomerangSummaryArgs.set("count", caughtCount); + boomerangSummaryArgs.set("score", score); + speak_with_history(trf("system.quest.boomerang.summary", boomerangSummaryArgs), true); return score; } diff --git a/src/quests/enchanted_melody_game.nvgt b/src/quests/enchanted_melody_game.nvgt index 78c738d..49bd824 100644 --- a/src/quests/enchanted_melody_game.nvgt +++ b/src/quests/enchanted_melody_game.nvgt @@ -1,21 +1,22 @@ // Enchanted Melody quest game -string[] quest_notes = { - "sounds/quests/bone1.ogg", - "sounds/quests/bone2.ogg", - "sounds/quests/bone3.ogg", - "sounds/quests/bone4.ogg" -}; +string[] quest_notes = {"sounds/quests/bone1.ogg", "sounds/quests/bone2.ogg", "sounds/quests/bone3.ogg", + "sounds/quests/bone4.ogg"}; void play_note(int note_index) { - if (note_index < 0 || note_index >= int(quest_notes.length())) return; + if (note_index < 0 || note_index >= int(quest_notes.length())) + return; p.play_stationary(quest_notes[note_index], false); } int get_note_from_key() { - if (key_pressed(KEY_F) || key_pressed(KEY_K)) return 0; - if (key_pressed(KEY_D) || key_pressed(KEY_J)) return 1; - if (key_pressed(KEY_R) || key_pressed(KEY_I)) return 2; - if (key_pressed(KEY_E) || key_pressed(KEY_U)) return 3; + if (key_pressed(KEY_F) || key_pressed(KEY_K)) + return 0; + if (key_pressed(KEY_D) || key_pressed(KEY_J)) + return 1; + if (key_pressed(KEY_R) || key_pressed(KEY_I)) + return 2; + if (key_pressed(KEY_E) || key_pressed(KEY_U)) + return 3; return -1; } @@ -27,17 +28,18 @@ 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); + speak_with_history(tr("system.quest.enchanted_melody.practice_prompt"), true); while (true) { wait(5); @@ -70,7 +72,7 @@ int run_enchanted_melody() { return 0; } - speak_with_history("Starting. Repeat the pattern.", true); + speak_with_history(tr("system.quest.enchanted_melody.starting_prompt"), true); wait(500); int[] pattern; @@ -89,11 +91,15 @@ int run_enchanted_melody() { wait(5); handle_global_volume_keys(); int note = get_note_from_key(); - if (note == -1) continue; + if (note == -1) + continue; play_note(note); if (note != pattern[index]) { int score = rounds * 2; - speak_with_history("You matched " + rounds + " notes. Score " + score + ".", true); + dictionary melodyScoreArgs; + melodyScoreArgs.set("rounds", rounds); + melodyScoreArgs.set("score", score); + speak_with_history(trf("system.quest.enchanted_melody.score", melodyScoreArgs), true); return score; } index++; diff --git a/src/quests/escape_from_hel_game.nvgt b/src/quests/escape_from_hel_game.nvgt index 5711a3c..e369166 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); @@ -42,7 +43,8 @@ int run_escape_from_hel() { while (true) { // Speed increases indefinitely until player can't keep up int step_time = base_step_time - (total_steps * 8); - if (step_time < 50) step_time = 50; // Minimum to prevent audio/timing issues + if (step_time < 50) + step_time = 50; // Minimum to prevent audio/timing issues handle_global_volume_keys(); @@ -114,7 +116,9 @@ int run_escape_from_hel() { pit_handle = -1; } p.play_2d("sounds/quests/fall.ogg", listener_x, listener_y, listener_x, listener_y, false); - speak_with_history("You fell in. Score " + score + ".", true); + dictionary fellInArgs; + fellInArgs.set("score", score); + speak_with_history(trf("system.quest.escape_from_hel.fell_in_score", fellInArgs), true); return score; } diff --git a/src/quests/skeletal_bard_game.nvgt b/src/quests/skeletal_bard_game.nvgt index 13d28f4..9a7afe5 100644 --- a/src/quests/skeletal_bard_game.nvgt +++ b/src/quests/skeletal_bard_game.nvgt @@ -1,14 +1,7 @@ // Skeletal Bard quest game -string[] skeletalBardNotes = { - "sounds/quests/bone1.ogg", - "sounds/quests/bone2.ogg", - "sounds/quests/bone3.ogg", - "sounds/quests/bone4.ogg", - "sounds/quests/bone5.ogg", - "sounds/quests/bone6.ogg", - "sounds/quests/bone7.ogg", - "sounds/quests/bone8.ogg" -}; +string[] skeletalBardNotes = {"sounds/quests/bone1.ogg", "sounds/quests/bone2.ogg", "sounds/quests/bone3.ogg", + "sounds/quests/bone4.ogg", "sounds/quests/bone5.ogg", "sounds/quests/bone6.ogg", + "sounds/quests/bone7.ogg", "sounds/quests/bone8.ogg"}; void play_skeletal_bard_note() { int noteIndex = random(0, skeletalBardNotes.length() - 1); @@ -17,7 +10,9 @@ void play_skeletal_bard_note() { int select_note_count(int minNotes, int maxNotes, int startValue) { int selection = startValue; - speak_with_history("Select number of notes. " + selection + ".", true); + dictionary selectArgs; + selectArgs.set("selection", selection); + speak_with_history(trf("system.quest.skeletal_bard.select_notes", selectArgs), true); while (true) { wait(5); @@ -26,12 +21,14 @@ int select_note_count(int minNotes, int maxNotes, int startValue) { } if (key_pressed(KEY_UP)) { - if (selection < maxNotes) selection++; + if (selection < maxNotes) + selection++; speak_with_history("" + selection + ".", true); } if (key_pressed(KEY_DOWN)) { - if (selection > minNotes) selection--; + if (selection > minNotes) + selection--; speak_with_history("" + selection + ".", true); } @@ -50,13 +47,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); @@ -78,13 +76,19 @@ int run_skeletal_bard() { return 0; } int diff = length - guess; - if (diff < 0) diff = -diff; + if (diff < 0) + diff = -diff; int points = 4 - diff; - if (points < 0) points = 0; + if (points < 0) + points = 0; score += points; - speak_with_history("You entered " + guess + " notes and the actual number was " + length + ". " + points + " points. Press Enter to continue.", true); + dictionary roundResultArgs; + roundResultArgs.set("guess", guess); + roundResultArgs.set("actual", length); + roundResultArgs.set("points", points); + speak_with_history(trf("system.quest.skeletal_bard.round_result", roundResultArgs), true); while (true) { wait(5); handle_global_volume_keys(); @@ -97,6 +101,8 @@ int run_skeletal_bard() { } } - speak_with_history("Skeletal Bard complete. Your score for all five tunes is " + score + ".", true); + dictionary bardSummaryArgs; + bardSummaryArgs.set("score", score); + speak_with_history(trf("system.quest.skeletal_bard.complete_score", bardSummaryArgs), true); return score; } diff --git a/src/runes/rune_data.nvgt b/src/runes/rune_data.nvgt index e00d2ab..56c92a1 100644 --- a/src/runes/rune_data.nvgt +++ b/src/runes/rune_data.nvgt @@ -38,15 +38,19 @@ int equipped_weapon_rune = RUNE_NONE; // Get display name for a rune type string get_rune_name(int rune_type) { - if (rune_type == RUNE_SWIFTNESS) return "Rune of Swiftness"; - if (rune_type == RUNE_DESTRUCTION) return "Rune of Destruction"; + if (rune_type == RUNE_SWIFTNESS) + return "Rune of Swiftness"; + if (rune_type == RUNE_DESTRUCTION) + return "Rune of Destruction"; return "Unknown Rune"; } // Get the effect suffix for runed item names (e.g., "of Quickness") string get_rune_effect_name(int rune_type) { - if (rune_type == RUNE_SWIFTNESS) return "Quickness"; - if (rune_type == RUNE_DESTRUCTION) return "Destruction"; + if (rune_type == RUNE_SWIFTNESS) + return "Quickness"; + if (rune_type == RUNE_DESTRUCTION) + return "Destruction"; return "Unknown"; } @@ -57,23 +61,29 @@ bool any_rune_unlocked() { // Check if a specific rune is unlocked bool is_rune_unlocked(int rune_type) { - if (rune_type == RUNE_SWIFTNESS) return rune_swiftness_unlocked; - if (rune_type == RUNE_DESTRUCTION) return rune_destruction_unlocked; + if (rune_type == RUNE_SWIFTNESS) + return rune_swiftness_unlocked; + if (rune_type == RUNE_DESTRUCTION) + return rune_destruction_unlocked; return false; } -void get_all_rune_types(int[]@ runeTypes) { - if (@runeTypes == null) return; +void get_all_rune_types(int[] @runeTypes) { + if (@runeTypes == null) + return; runeTypes.resize(0); runeTypes.insert_last(RUNE_SWIFTNESS); runeTypes.insert_last(RUNE_DESTRUCTION); } -void get_unlocked_rune_types(int[]@ runeTypes) { - if (@runeTypes == null) return; +void get_unlocked_rune_types(int[] @runeTypes) { + if (@runeTypes == null) + return; runeTypes.resize(0); - if (rune_swiftness_unlocked) runeTypes.insert_last(RUNE_SWIFTNESS); - if (rune_destruction_unlocked) runeTypes.insert_last(RUNE_DESTRUCTION); + if (rune_swiftness_unlocked) + runeTypes.insert_last(RUNE_SWIFTNESS); + if (rune_destruction_unlocked) + runeTypes.insert_last(RUNE_DESTRUCTION); } // Create dictionary key for runed item storage @@ -111,7 +121,8 @@ bool has_any_runed_version(int equip_type) { int[] runeTypes; get_all_rune_types(runeTypes); for (uint i = 0; i < runeTypes.length(); i++) { - if (get_runed_item_count(equip_type, runeTypes[i]) > 0) return true; + if (get_runed_item_count(equip_type, runeTypes[i]) > 0) + return true; } return false; } @@ -144,7 +155,8 @@ void remove_stored_runed_item(int equip_type, int rune_type) { // Deposit runed item from personal inventory to storage bool deposit_runed_item(int equip_type, int rune_type) { int personal = get_runed_item_count(equip_type, rune_type); - if (personal <= 0) return false; + if (personal <= 0) + return false; remove_runed_item(equip_type, rune_type); add_stored_runed_item(equip_type, rune_type); @@ -154,7 +166,8 @@ bool deposit_runed_item(int equip_type, int rune_type) { // Withdraw runed item from storage to personal inventory bool withdraw_runed_item(int equip_type, int rune_type) { int stored = get_stored_runed_item_count(equip_type, rune_type); - if (stored <= 0) return false; + if (stored <= 0) + return false; remove_stored_runed_item(equip_type, rune_type); add_runed_item(equip_type, rune_type); @@ -163,32 +176,54 @@ bool withdraw_runed_item(int equip_type, int rune_type) { // Get the rune type on equipped item for a given slot int get_equipped_rune_for_slot(int equip_type) { - if (equip_type == EQUIP_SPEAR || equip_type == EQUIP_AXE || - equip_type == EQUIP_SLING || equip_type == EQUIP_BOW) { + if (equip_type == EQUIP_SPEAR || equip_type == EQUIP_AXE || equip_type == EQUIP_SLING || equip_type == EQUIP_BOW) { return equipped_weapon_rune; } - if (equip_type == EQUIP_HAT) return equipped_head_rune; - if (equip_type == EQUIP_TUNIC) return equipped_torso_rune; - if (equip_type == EQUIP_POUCH || equip_type == EQUIP_BACKPACK) return equipped_arms_rune; - if (equip_type == EQUIP_GLOVES) return equipped_hands_rune; - if (equip_type == EQUIP_PANTS) return equipped_legs_rune; - if (equip_type == EQUIP_MOCCASINS) return equipped_feet_rune; + if (equip_type == EQUIP_HAT) + return equipped_head_rune; + if (equip_type == EQUIP_TUNIC) + return equipped_torso_rune; + if (equip_type == EQUIP_POUCH || equip_type == EQUIP_BACKPACK) + return equipped_arms_rune; + if (equip_type == EQUIP_GLOVES) + return equipped_hands_rune; + if (equip_type == EQUIP_PANTS) + return equipped_legs_rune; + if (equip_type == EQUIP_MOCCASINS) + return equipped_feet_rune; return RUNE_NONE; } // Set the rune on an equipped item slot void set_equipped_rune_for_slot(int equip_type, int rune_type) { - if (equip_type == EQUIP_SPEAR || equip_type == EQUIP_AXE || - equip_type == EQUIP_SLING || equip_type == EQUIP_BOW) { + if (equip_type == EQUIP_SPEAR || equip_type == EQUIP_AXE || equip_type == EQUIP_SLING || equip_type == EQUIP_BOW) { equipped_weapon_rune = rune_type; return; } - if (equip_type == EQUIP_HAT) { equipped_head_rune = rune_type; return; } - if (equip_type == EQUIP_TUNIC) { equipped_torso_rune = rune_type; return; } - if (equip_type == EQUIP_POUCH || equip_type == EQUIP_BACKPACK) { equipped_arms_rune = rune_type; return; } - if (equip_type == EQUIP_GLOVES) { equipped_hands_rune = rune_type; return; } - if (equip_type == EQUIP_PANTS) { equipped_legs_rune = rune_type; return; } - if (equip_type == EQUIP_MOCCASINS) { equipped_feet_rune = rune_type; return; } + if (equip_type == EQUIP_HAT) { + equipped_head_rune = rune_type; + return; + } + if (equip_type == EQUIP_TUNIC) { + equipped_torso_rune = rune_type; + return; + } + if (equip_type == EQUIP_POUCH || equip_type == EQUIP_BACKPACK) { + equipped_arms_rune = rune_type; + return; + } + if (equip_type == EQUIP_GLOVES) { + equipped_hands_rune = rune_type; + return; + } + if (equip_type == EQUIP_PANTS) { + equipped_legs_rune = rune_type; + return; + } + if (equip_type == EQUIP_MOCCASINS) { + equipped_feet_rune = rune_type; + return; + } } // Clear rune from an equipped slot diff --git a/src/runes/rune_effects.nvgt b/src/runes/rune_effects.nvgt index d31b84f..bc95355 100644 --- a/src/runes/rune_effects.nvgt +++ b/src/runes/rune_effects.nvgt @@ -8,13 +8,20 @@ int get_total_rune_walk_speed_bonus() { int bonus = 0; // Check each equipment slot for swiftness runes - if (equipped_head_rune == RUNE_SWIFTNESS) bonus += RUNE_SWIFTNESS_SPEED_BONUS; - if (equipped_torso_rune == RUNE_SWIFTNESS) bonus += RUNE_SWIFTNESS_SPEED_BONUS; - if (equipped_arms_rune == RUNE_SWIFTNESS) bonus += RUNE_SWIFTNESS_SPEED_BONUS; - if (equipped_hands_rune == RUNE_SWIFTNESS) bonus += RUNE_SWIFTNESS_SPEED_BONUS; - if (equipped_legs_rune == RUNE_SWIFTNESS) bonus += RUNE_SWIFTNESS_SPEED_BONUS; - if (equipped_feet_rune == RUNE_SWIFTNESS) bonus += RUNE_SWIFTNESS_SPEED_BONUS; - if (equipped_weapon_rune == RUNE_SWIFTNESS) bonus += RUNE_SWIFTNESS_SPEED_BONUS; + if (equipped_head_rune == RUNE_SWIFTNESS) + bonus += RUNE_SWIFTNESS_SPEED_BONUS; + if (equipped_torso_rune == RUNE_SWIFTNESS) + bonus += RUNE_SWIFTNESS_SPEED_BONUS; + if (equipped_arms_rune == RUNE_SWIFTNESS) + bonus += RUNE_SWIFTNESS_SPEED_BONUS; + if (equipped_hands_rune == RUNE_SWIFTNESS) + bonus += RUNE_SWIFTNESS_SPEED_BONUS; + if (equipped_legs_rune == RUNE_SWIFTNESS) + bonus += RUNE_SWIFTNESS_SPEED_BONUS; + if (equipped_feet_rune == RUNE_SWIFTNESS) + bonus += RUNE_SWIFTNESS_SPEED_BONUS; + if (equipped_weapon_rune == RUNE_SWIFTNESS) + bonus += RUNE_SWIFTNESS_SPEED_BONUS; return bonus; } @@ -25,13 +32,20 @@ int get_total_rune_gather_bonus() { int bonus = 0; // Check each equipment slot for swiftness runes - if (equipped_head_rune == RUNE_SWIFTNESS) bonus += RUNE_SWIFTNESS_GATHER_BONUS; - if (equipped_torso_rune == RUNE_SWIFTNESS) bonus += RUNE_SWIFTNESS_GATHER_BONUS; - if (equipped_arms_rune == RUNE_SWIFTNESS) bonus += RUNE_SWIFTNESS_GATHER_BONUS; - if (equipped_hands_rune == RUNE_SWIFTNESS) bonus += RUNE_SWIFTNESS_GATHER_BONUS; - if (equipped_legs_rune == RUNE_SWIFTNESS) bonus += RUNE_SWIFTNESS_GATHER_BONUS; - if (equipped_feet_rune == RUNE_SWIFTNESS) bonus += RUNE_SWIFTNESS_GATHER_BONUS; - if (equipped_weapon_rune == RUNE_SWIFTNESS) bonus += RUNE_SWIFTNESS_GATHER_BONUS; + if (equipped_head_rune == RUNE_SWIFTNESS) + bonus += RUNE_SWIFTNESS_GATHER_BONUS; + if (equipped_torso_rune == RUNE_SWIFTNESS) + bonus += RUNE_SWIFTNESS_GATHER_BONUS; + if (equipped_arms_rune == RUNE_SWIFTNESS) + bonus += RUNE_SWIFTNESS_GATHER_BONUS; + if (equipped_hands_rune == RUNE_SWIFTNESS) + bonus += RUNE_SWIFTNESS_GATHER_BONUS; + if (equipped_legs_rune == RUNE_SWIFTNESS) + bonus += RUNE_SWIFTNESS_GATHER_BONUS; + if (equipped_feet_rune == RUNE_SWIFTNESS) + bonus += RUNE_SWIFTNESS_GATHER_BONUS; + if (equipped_weapon_rune == RUNE_SWIFTNESS) + bonus += RUNE_SWIFTNESS_GATHER_BONUS; return bonus; } @@ -43,10 +57,12 @@ int apply_rune_gather_bonus(int base_time) { if (blessing_search_active) { bonus_percent += BLESSING_SEARCH_GATHER_BONUS; } - if (bonus_percent <= 0) return base_time; + if (bonus_percent <= 0) + return base_time; // Keep gathering from becoming instant. - if (bonus_percent > GATHER_TIME_REDUCTION_CAP) bonus_percent = GATHER_TIME_REDUCTION_CAP; + if (bonus_percent > GATHER_TIME_REDUCTION_CAP) + bonus_percent = GATHER_TIME_REDUCTION_CAP; int reduction = (base_time * bonus_percent) / 100; return base_time - reduction; @@ -55,13 +71,20 @@ int apply_rune_gather_bonus(int base_time) { // Count total equipped swiftness runes int count_equipped_swiftness_runes() { int count = 0; - if (equipped_head_rune == RUNE_SWIFTNESS) count++; - if (equipped_torso_rune == RUNE_SWIFTNESS) count++; - if (equipped_arms_rune == RUNE_SWIFTNESS) count++; - if (equipped_hands_rune == RUNE_SWIFTNESS) count++; - if (equipped_legs_rune == RUNE_SWIFTNESS) count++; - if (equipped_feet_rune == RUNE_SWIFTNESS) count++; - if (equipped_weapon_rune == RUNE_SWIFTNESS) count++; + if (equipped_head_rune == RUNE_SWIFTNESS) + count++; + if (equipped_torso_rune == RUNE_SWIFTNESS) + count++; + if (equipped_arms_rune == RUNE_SWIFTNESS) + count++; + if (equipped_hands_rune == RUNE_SWIFTNESS) + count++; + if (equipped_legs_rune == RUNE_SWIFTNESS) + count++; + if (equipped_feet_rune == RUNE_SWIFTNESS) + count++; + if (equipped_weapon_rune == RUNE_SWIFTNESS) + count++; return count; } @@ -79,14 +102,16 @@ string get_rune_speed_description() { desc += walk_bonus + "ms faster walking"; } if (gather_bonus > 0) { - if (desc.length() > 0) desc += ", "; + if (desc.length() > 0) + desc += ", "; desc += gather_bonus + "% faster gathering"; } return desc; } int apply_weapon_rune_damage(int baseDamage) { - if (baseDamage <= 0) return baseDamage; + if (baseDamage <= 0) + return baseDamage; if (equipped_weapon_rune == RUNE_DESTRUCTION) { return baseDamage * RUNE_DESTRUCTION_DAMAGE_MULTIPLIER; } diff --git a/src/save_system.nvgt b/src/save_system.nvgt index 858865d..4a76079 100644 --- a/src/save_system.nvgt +++ b/src/save_system.nvgt @@ -1,18 +1,142 @@ // 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"; const int SAVE_VERSION = 3; +const string SAVE_COMPANY_NAME = "stormux"; +const string SAVE_PRODUCT_NAME = "Draugnorak"; +const string SAVE_CRASH_LOG_FILENAME = "crash.log"; string last_save_error = ""; string current_save_file = ""; +string save_directory_path = ""; +bool save_directory_path_ready = false; +bool legacy_save_migration_checked = false; + +bool is_absolute_path(const string& in path) { + if (path.length() == 0) + return false; + string first = path.substr(0, 1); + if (first == "/" || first == "\\") + return true; + if (path.length() >= 2 && path.substr(1, 1) == ":") + return true; + return false; +} + +string 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 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 get_save_directory_path() { + if (save_directory_path_ready && save_directory_path != "") { + return save_directory_path; + } + + string path = join({DIRECTORY_APPDATA, SAVE_COMPANY_NAME, SAVE_PRODUCT_NAME}, "/"); + + path = trim_trailing_path_separator(path); + if (path == "") { + path = "."; + } + + if (!directory_exists(path) && !directory_create(path)) { + path = "."; + } + + save_directory_path = path; + save_directory_path_ready = true; + return save_directory_path; +} + +string resolve_save_path(const string& in filename) { + if (filename == "") + return get_save_directory_path(); + if (is_absolute_path(filename)) + return filename; + + string saveDir = get_save_directory_path(); + if (saveDir == "." || saveDir == "") { + return filename; + } + return saveDir + "/" + filename; +} + +string get_crash_log_path() { + return resolve_save_path(SAVE_CRASH_LOG_FILENAME); +} + +void migrate_legacy_save_files_if_needed() { + if (legacy_save_migration_checked) + return; + legacy_save_migration_checked = true; + + string saveDir = get_save_directory_path(); + if (saveDir == "." || saveDir == "") + return; + + string[] @legacyItems = glob("*" + SAVE_EXTENSION); + if (@legacyItems == null) + return; + + for (uint i = 0; i < legacyItems.length(); i++) { + string legacyPath = legacyItems[i]; + string legacyName = get_path_basename(legacyPath); + if (legacyName == "") + continue; + + string targetPath = resolve_save_path(legacyName); + if (file_exists(targetPath)) + continue; + + file legacyFile; + if (!legacyFile.open(legacyPath, "rb")) + continue; + string legacyData = legacyFile.read(); + legacyFile.close(); + if (legacyData.length() == 0) + continue; + + save_data(legacyName, legacyData); + } +} string[] get_save_files() { + migrate_legacy_save_files_if_needed(); + string[] result; - string[]@ items = glob("*" + SAVE_EXTENSION); - if (@items == null) return result; + string[] @items = glob(resolve_save_path("*" + SAVE_EXTENSION)); + if (@items == null) + return result; for (uint i = 0; i < items.length(); i++) { - string item = items[i]; + string item = get_path_basename(items[i]); if (item.length() >= SAVE_EXTENSION.length() && item.substr(item.length() - SAVE_EXTENSION.length()) == SAVE_EXTENSION) { result.insert_last(item); @@ -24,246 +148,95 @@ string[] get_save_files() { return result; } -bool sort_string_case_insensitive(const string &in a, const string &in b) { - return a.lower() < b.lower(); +bool sort_string_case_insensitive(const string& in a, const string& in b) { + return save_utils_sort_string_case_insensitive(a, b); } bool has_save_game() { return get_save_files().length() > 0; } -string encrypt_save_data(const string&in rawData) { - return string_aes_encrypt(rawData, SAVE_ENCRYPTION_KEY); +string encrypt_save_data(const string& in rawData) { + return encrypt_string_aes(rawData, SAVE_ENCRYPTION_KEY); } -string decrypt_save_data(const string&in encryptedData) { - return string_aes_decrypt(encryptedData, SAVE_ENCRYPTION_KEY); +string decrypt_save_data(const string& in encryptedData) { + 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; - } - file tmp; - if (!tmp.open(filename, "wb")) { - return false; - } - - if (tmp.write(data) < data.length()) { - tmp.close(); - return false; - } - - tmp.close(); - return true; +bool save_data(const string& in filename, const string& in data) { + return save_string_file(resolve_save_path(filename), data); } -bool read_file_string(const string&in filename, string&out data) { - file tmp; - if (!tmp.open(filename, "rb")) { - return false; - } - data = tmp.read(); - tmp.close(); - return data.length() > 0; +bool read_file_string(const string& in filename, string& out data) { + 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; +double get_number(dictionary @data, const string& in key, double 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; +bool get_bool(dictionary @data, const string& in key, bool 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; +bool dictionary_has_keys(dictionary @data) { + 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; +bool has_number_key(dictionary @data, const string& in key) { + 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; +string[] get_string_list(dictionary @data, const string& in key) { + 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; +string flatten_exception_text(const string& in text) { + 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("crash.log", "ab")) { - logFile.write(message + "\r\n"); - logFile.close(); - } +void log_unhandled_exception(const string& in context) { + 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; +bool is_windows_reserved_name(const string& in upperName) { + 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; +string get_save_filename_for_name(const string& in name) { + return build_filename_from_name_ex(name, SAVE_EXTENSION, 40, "character", "save_"); } -string strip_save_extension(const string&in filename) { - if (filename.length() >= SAVE_EXTENSION.length() && - filename.substr(filename.length() - SAVE_EXTENSION.length()) == SAVE_EXTENSION) { - return filename.substr(0, filename.length() - SAVE_EXTENSION.length()); +string strip_save_extension(const string& in filename) { + string baseName = get_path_basename(filename); + if (baseName.length() >= SAVE_EXTENSION.length() && + baseName.substr(baseName.length() - SAVE_EXTENSION.length()) == SAVE_EXTENSION) { + return baseName.substr(0, baseName.length() - SAVE_EXTENSION.length()); } - return filename; + return baseName; } -bool read_save_metadata(const string&in filename, string &out displayName, int &out sex, int &out day) { +bool read_save_metadata(const string& in filename, string& out displayName, int& out sex, int& out day) { string encryptedData; if (!read_file_string(filename, encryptedData)) { return false; } string rawData = decrypt_save_data(encryptedData); - dictionary@ saveData = deserialize(rawData); + dictionary @saveData = deserialize(rawData); if (@saveData == null || !dictionary_has_keys(saveData)) { saveData = deserialize(encryptedData); } @@ -276,27 +249,33 @@ bool read_save_metadata(const string&in filename, string &out displayName, int & displayName = ""; } sex = int(get_number(saveData, "player_sex", SEX_MALE)); - if (sex != SEX_FEMALE) sex = SEX_MALE; + if (sex != SEX_FEMALE) + sex = SEX_MALE; day = int(get_number(saveData, "time_current_day", 1)); - if (day < 1) day = 1; + if (day < 1) + day = 1; if (displayName == "") { displayName = strip_save_extension(filename); } return true; } -bool is_name_used(const string&in name, const string[]@ usedNames) { +bool is_name_used(const string& in name, const string[] @usedNames) { + migrate_legacy_save_files_if_needed(); + string target = name.lower(); if (@usedNames != null) { for (uint i = 0; i < usedNames.length(); i++) { - if (usedNames[i].lower() == target) return true; + if (usedNames[i].lower() == target) + return true; } } - if (file_exists(get_save_filename_for_name(name))) return true; + if (file_exists(resolve_save_path(get_save_filename_for_name(name)))) + return true; return false; } -string pick_random_name(const string[]@ pool, const string[]@ usedNames) { +string pick_random_name(const string[] @pool, const string[] @usedNames) { string[] available; if (@pool != null) { for (uint i = 0; i < pool.length(); i++) { @@ -305,11 +284,12 @@ string pick_random_name(const string[]@ pool, const string[]@ usedNames) { } } } - const string[]@ pickFrom = @available; + const string[] @pickFrom = @available; if (available.length() == 0) { @pickFrom = pool; } - if (@pickFrom == null || pickFrom.length() == 0) return "character"; + if (@pickFrom == null || pickFrom.length() == 0) + return "character"; int index = random(0, int(pickFrom.length()) - 1); return pickFrom[index]; } @@ -331,32 +311,36 @@ string[] get_existing_character_names() { } string[] male_name_pool = { - "Arne", "Asbjorn", "Aegir", "Bjorn", "Brand", "Egil", "Einar", "Eirik", "Erik", "Gunnar", - "Gudmund", "Hakon", "Halfdan", "Hallvard", "Harald", "Hjalmar", "Hrafn", "Hrolf", "Ivar", "Ketil", - "Knut", "Leif", "Magnus", "Njord", "Odd", "Olaf", "Orm", "Ragnar", "Roald", "Rolf", - "Sigurd", "Sten", "Stig", "Sven", "Svend", "Thor", "Toke", "Torbjorn", "Torstein", "Trygve", - "Ulf", "Ulrik", "Valdemar", "Vidar", "Yngvar", "Haldor", "Skjold", "Eystein", "Gorm", "Havard" -}; + "Arne", "Asbjorn", "Aegir", "Bjorn", "Brand", "Egil", "Einar", "Eirik", "Erik", "Gunnar", + "Gudmund", "Hakon", "Halfdan", "Hallvard", "Harald", "Hjalmar", "Hrafn", "Hrolf", "Ivar", "Ketil", + "Knut", "Leif", "Magnus", "Njord", "Odd", "Olaf", "Orm", "Ragnar", "Roald", "Rolf", + "Sigurd", "Sten", "Stig", "Sven", "Svend", "Thor", "Toke", "Torbjorn", "Torstein", "Trygve", + "Ulf", "Ulrik", "Valdemar", "Vidar", "Yngvar", "Haldor", "Skjold", "Eystein", "Gorm", "Havard"}; string[] female_name_pool = { - "Astrid", "Asta", "Birgit", "Brynhild", "Dagny", "Eira", "Freya", "Frida", "Gerda", "Gudrun", - "Gunhild", "Halla", "Helga", "Hild", "Hilda", "Inga", "Ingrid", "Kari", "Lagertha", "Liv", - "Ragna", "Ragnhild", "Randi", "Runa", "Sif", "Signy", "Sigrid", "Solveig", "Sunniva", "Thora", - "Thyra", "Tora", "Tove", "Tyrna", "Ulla", "Yrsa", "Ylva", "Aud", "Eydis", "Herdis", - "Ingunn", "Jorunn", "Ragnheid", "Sigrun", "Torhild", "Ase", "Alfhild", "Gudlaug", "Katra", "Rikissa" -}; + "Astrid", "Asta", "Birgit", "Brynhild", "Dagny", "Eira", "Freya", "Frida", "Gerda", "Gudrun", + "Gunhild", "Halla", "Helga", "Hild", "Hilda", "Inga", "Ingrid", "Kari", "Lagertha", "Liv", + "Ragna", "Ragnhild", "Randi", "Runa", "Sif", "Signy", "Sigrid", "Solveig", "Sunniva", "Thora", + "Thyra", "Tora", "Tove", "Tyrna", "Ulla", "Yrsa", "Ylva", "Aud", "Eydis", "Herdis", + "Ingunn", "Jorunn", "Ragnheid", "Sigrun", "Torhild", "Ase", "Alfhild", "Gudlaug", "Katra", "Rikissa"}; -string pick_random_name_for_sex(int sex, const string[]@ usedNames) { +string pick_random_name_for_sex(int sex, const string[] @usedNames) { if (sex == SEX_FEMALE) { return pick_random_name(female_name_pool, usedNames); } return pick_random_name(male_name_pool, usedNames); } -bool select_player_sex(int &out sex) { - string[] options = {"Male", "Female"}; +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 = {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) { @@ -364,13 +348,15 @@ bool select_player_sex(int &out sex) { 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); } if (key_pressed(KEY_RETURN)) { @@ -386,6 +372,8 @@ bool select_player_sex(int &out sex) { } bool setup_new_character() { + migrate_legacy_save_files_if_needed(); + int selectedSex = SEX_MALE; if (!select_player_sex(selectedSex)) { return false; @@ -393,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(saveFile)) { - int confirm = ui_question("", "Save found for " + normalized + ". Overwrite?"); + if (file_exists(resolve_save_path(saveFile))) { + dictionary overwriteArgs; + overwriteArgs.set("name", normalized); + int confirm = ui_question("", trf("system.new_character.save_exists_overwrite", overwriteArgs)); if (confirm != 1) { continue; } @@ -413,10 +403,11 @@ bool setup_new_character() { return false; } -bool select_save_file(string &out filename) { +bool select_save_file(string& out filename) { while (true) { string[] files = get_save_files(); - if (files.length() == 0) return false; + if (files.length() == 0) + return false; string[] options; string[] displayNames; @@ -432,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); @@ -442,7 +436,7 @@ bool select_save_file(string &out filename) { } int selection = 0; - speak_with_history("Load game. Select character. Press delete to remove a save.", true); + speak_with_history(tr("system.load_game.select_prompt"), true); speak_with_history(options[selection], true); bool refreshList = false; @@ -451,13 +445,15 @@ bool select_save_file(string &out filename) { 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 = int(options.length()) - 1; + if (selection < 0) + selection = int(options.length()) - 1; speak_with_history(options[selection], true); } if (key_pressed(KEY_RETURN)) { @@ -466,21 +462,23 @@ 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) { - if (file_delete(files[selection])) { - speak_with_history("Save deleted.", true); + if (file_delete(resolve_save_path(files[selection]))) { + speak_with_history(tr("system.load_game.save_deleted"), true); 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 { @@ -725,54 +723,62 @@ string serialize_bool(bool value) { return value ? "1" : "0"; } -string serialize_tree(Tree@ tree) { - return tree.position + "|" + tree.sticks + "|" + tree.vines + "|" + tree.health + "|" + tree.height + "|" + serialize_bool(tree.depleted) + "|" + serialize_bool(tree.is_chopped) + "|" + tree.minutes_since_depletion; +string serialize_tree(Tree @tree) { + return tree.position + "|" + tree.sticks + "|" + tree.vines + "|" + tree.health + "|" + tree.height + "|" + + serialize_bool(tree.depleted) + "|" + serialize_bool(tree.is_chopped) + "|" + tree.minutes_since_depletion; } -string serialize_snare(WorldSnare@ snare) { - return snare.position + "|" + serialize_bool(snare.has_catch) + "|" + snare.catch_type + "|" + snare.catch_chance + "|" + snare.escape_chance + "|" + serialize_bool(snare.active) + "|" + snare.hours_with_catch; +string serialize_snare(WorldSnare @snare) { + return snare.position + "|" + serialize_bool(snare.has_catch) + "|" + snare.catch_type + "|" + snare.catch_chance + + "|" + snare.escape_chance + "|" + serialize_bool(snare.active) + "|" + snare.hours_with_catch; } -string serialize_fire(WorldFire@ fire) { +string serialize_fire(WorldFire @fire) { return fire.position + "|" + fire.fuel_remaining + "|" + serialize_bool(fire.low_fuel_warned); } -string serialize_stream(WorldStream@ stream) { +string serialize_stream(WorldStream @stream) { return stream.start_position + "|" + stream.get_width(); } -string serialize_bandit(Bandit@ bandit) { - return bandit.position + "|" + bandit.health + "|" + bandit.weapon_type + "|" + bandit.behavior_state + "|" + bandit.wander_direction + "|" + bandit.move_interval + "|" + bandit.invader_type + "|" + bandit.home_start + "|" + bandit.home_end; +string serialize_bandit(Bandit @bandit) { + return bandit.position + "|" + bandit.health + "|" + bandit.weapon_type + "|" + bandit.behavior_state + "|" + + bandit.wander_direction + "|" + bandit.move_interval + "|" + bandit.invader_type + "|" + bandit.home_start + + "|" + bandit.home_end; } -string serialize_mountain(MountainRange@ mountain) { +string serialize_mountain(MountainRange @mountain) { string result = mountain.start_position + "|" + mountain.end_position + "|"; // Serialize elevations for (int i = 0; i < int(mountain.elevations.length()); i++) { - if (i > 0) result += ","; + if (i > 0) + result += ","; result += mountain.elevations[i]; } result += "|"; // Serialize terrain types for (int i = 0; i < int(mountain.terrain_types.length()); i++) { - if (i > 0) result += ","; + if (i > 0) + result += ","; result += mountain.terrain_types[i]; } result += "|"; // Serialize stream positions for (uint i = 0; i < mountain.stream_positions.length(); i++) { - if (i > 0) result += ","; + if (i > 0) + result += ","; result += mountain.stream_positions[i]; } return result; } -string join_string_array(const string[]@ arr) { - if (@arr == null || arr.length() == 0) return ""; +string join_string_array(const string[] @arr) { + if (@arr == null || arr.length() == 0) + return ""; string result = arr[0]; for (uint i = 1; i < arr.length(); i++) { result += "\n" + arr[i]; @@ -780,16 +786,18 @@ string join_string_array(const string[]@ arr) { return result; } -string[] split_string_array(const string&in data) { +string[] split_string_array(const string& in data) { string[] result; - if (data.length() == 0) return result; + if (data.length() == 0) + return result; result = data.split("\n"); return result; } -string[] get_string_list_or_split(dictionary@ data, const string&in key) { +string[] get_string_list_or_split(dictionary @data, const string& in key) { string[] result = get_string_list(data, key); - if (result.length() > 0) return result; + if (result.length() > 0) + return result; string value; if (@data != null && data.get(key, value)) { return split_string_array(value); @@ -798,8 +806,9 @@ string[] get_string_list_or_split(dictionary@ data, const string&in key) { } // Serialize inventory array to comma-separated string -string serialize_inventory_array(const int[]@ arr) { - if (@arr == null || arr.length() == 0) return ""; +string serialize_inventory_array(const int[] @arr) { + if (@arr == null || arr.length() == 0) + return ""; string result = "" + arr[0]; for (uint i = 1; i < arr.length(); i++) { result += "," + arr[i]; @@ -808,16 +817,18 @@ string serialize_inventory_array(const int[]@ arr) { } // Deserialize comma-separated string to inventory array -void deserialize_inventory_array(const string&in data, int[]@ arr, int expected_size) { +void deserialize_inventory_array(const string& in data, int[] @arr, int expected_size) { arr.resize(expected_size); for (int i = 0; i < expected_size; i++) { arr[i] = 0; } - if (data.length() == 0) return; + if (data.length() == 0) + return; - string[]@ parts = data.split(","); + string[] @parts = data.split(","); uint count = parts.length(); - if (count > uint(expected_size)) count = uint(expected_size); + if (count > uint(expected_size)) + count = uint(expected_size); for (uint i = 0; i < count; i++) { arr[i] = parse_int(parts[i]); } @@ -906,7 +917,7 @@ bool save_game_state() { // Save runed items dictionary as key:value pairs string[] runed_items_data; - string[]@ keys = runed_items.get_keys(); + string[] @keys = runed_items.get_keys(); for (uint i = 0; i < keys.length(); i++) { string key = keys[i]; int count = int(runed_items[key]); @@ -918,7 +929,7 @@ bool save_game_state() { // Save stored runed items dictionary string[] stored_runed_items_data; - string[]@ stored_keys = stored_runed_items.get_keys(); + string[] @stored_keys = stored_runed_items.get_keys(); for (uint i = 0; i < stored_keys.length(); i++) { string key = stored_keys[i]; int count = int(stored_runed_items[key]); @@ -1064,9 +1075,10 @@ bool save_game_state() { return save_data(current_save_file, encryptedData); } -bool load_game_state_from_file(const string&in filename) { +bool load_game_state_from_file(const string& in filename) { last_save_error = ""; - if (!file_exists(filename)) { + string resolvedFilename = resolve_save_path(filename); + if (!file_exists(resolvedFilename)) { last_save_error = "No save file found."; return false; } @@ -1078,7 +1090,7 @@ bool load_game_state_from_file(const string&in filename) { } string rawData = decrypt_save_data(encryptedData); - dictionary@ saveData = deserialize(rawData); + dictionary @saveData = deserialize(rawData); if (@saveData == null || !dictionary_has_keys(saveData)) { saveData = deserialize(encryptedData); } @@ -1089,7 +1101,8 @@ bool load_game_state_from_file(const string&in filename) { double version; bool has_version = saveData.get("version", version); if (!has_version) { - if (!has_number_key(saveData, "player_x") || !has_number_key(saveData, "player_health") || !has_number_key(saveData, "time_current_day")) { + if (!has_number_key(saveData, "player_x") || !has_number_key(saveData, "player_health") || + !has_number_key(saveData, "time_current_day")) { last_save_error = "Save file is missing required data."; return false; } @@ -1097,7 +1110,7 @@ bool load_game_state_from_file(const string&in filename) { } reset_game_state(); - current_save_file = filename; + current_save_file = get_path_basename(filename); MAP_SIZE = int(get_number(saveData, "world_map_size", 35)); expanded_area_start = int(get_number(saveData, "world_expanded_area_start", -1)); @@ -1112,22 +1125,31 @@ bool load_game_state_from_file(const string&in filename) { barricade_initialized = get_bool(saveData, "world_barricade_initialized", true); barricade_health = int(get_number(saveData, "world_barricade_health", BARRICADE_BASE_HEALTH)); residents_count = int(get_number(saveData, "world_residents_count", 0)); - if (residents_count < 0) residents_count = 0; + if (residents_count < 0) + residents_count = 0; undead_residents_count = int(get_number(saveData, "world_undead_residents_count", 0)); - if (undead_residents_count < 0) undead_residents_count = 0; + if (undead_residents_count < 0) + undead_residents_count = 0; undead_residents_pending = int(get_number(saveData, "world_undead_residents_pending", 0)); - if (undead_residents_pending < 0) undead_residents_pending = 0; + if (undead_residents_pending < 0) + undead_residents_pending = 0; horses_count = int(get_number(saveData, "world_horses_count", 0)); livestock_count = int(get_number(saveData, "world_livestock_count", 0)); - if (horses_count < 0) horses_count = 0; - if (livestock_count < 0) livestock_count = 0; - if (horses_count > MAX_HORSES) horses_count = MAX_HORSES; - if (livestock_count > MAX_LIVESTOCK) livestock_count = MAX_LIVESTOCK; + if (horses_count < 0) + horses_count = 0; + if (livestock_count < 0) + livestock_count = 0; + if (horses_count > MAX_HORSES) + horses_count = MAX_HORSES; + if (livestock_count > MAX_LIVESTOCK) + livestock_count = MAX_LIVESTOCK; if (!barricade_initialized) { init_barricade(); } else { - if (barricade_health < 0) barricade_health = 0; - if (barricade_health > BARRICADE_MAX_HEALTH) barricade_health = BARRICADE_MAX_HEALTH; + if (barricade_health < 0) + barricade_health = 0; + if (barricade_health > BARRICADE_MAX_HEALTH) + barricade_health = BARRICADE_MAX_HEALTH; } x = int(get_number(saveData, "player_x", 0)); @@ -1143,7 +1165,8 @@ bool load_game_state_from_file(const string&in filename) { player_name = strip_save_extension(filename); } player_sex = int(get_number(saveData, "player_sex", SEX_MALE)); - if (player_sex != SEX_FEMALE) player_sex = SEX_MALE; + if (player_sex != SEX_FEMALE) + player_sex = SEX_MALE; favor = get_number(saveData, "player_favor", 0.0); last_adventure_day = int(get_number(saveData, "player_last_adventure_day", -1)); string adventureCountsStr; @@ -1152,7 +1175,8 @@ bool load_game_state_from_file(const string&in filename) { } incense_hours_remaining = int(get_number(saveData, "incense_hours_remaining", 0)); incense_burning = get_bool(saveData, "incense_burning", false); - if (incense_hours_remaining > 0) incense_burning = true; + if (incense_hours_remaining > 0) + incense_burning = true; petActive = get_bool(saveData, "pet_active", false); string loadedPetSound; petSoundPath = ""; @@ -1180,7 +1204,7 @@ bool load_game_state_from_file(const string&in filename) { petHealth = 0; petKnockoutHoursRemaining = 0; } - if (petActive && petSoundPath != "" && !file_exists(petSoundPath)) { + if (petActive && petSoundPath != "" && !audio_asset_exists(petSoundPath)) { petActive = false; petSoundPath = ""; petType = ""; @@ -1190,24 +1214,31 @@ bool load_game_state_from_file(const string&in filename) { petKnockoutHoursRemaining = 0; } if (petActive) { - if (petKnockoutHoursRemaining < 0) petKnockoutHoursRemaining = 0; + if (petKnockoutHoursRemaining < 0) + petKnockoutHoursRemaining = 0; if (petKnockoutHoursRemaining > PET_KNOCKOUT_COOLDOWN_HOURS) { petKnockoutHoursRemaining = PET_KNOCKOUT_COOLDOWN_HOURS; } if (petKnockoutHoursRemaining > 0) { petHealth = 0; } else { - if (petHealth <= 0) petHealth = PET_HEALTH_MAX; - if (petHealth > PET_HEALTH_MAX) petHealth = PET_HEALTH_MAX; + if (petHealth <= 0) + petHealth = PET_HEALTH_MAX; + if (petHealth > PET_HEALTH_MAX) + petHealth = PET_HEALTH_MAX; } petAttackTimer.restart(); petRetrieveTimer.restart(); } - if (x < 0) x = 0; - if (x >= MAP_SIZE) x = MAP_SIZE - 1; - if (y < 0) y = 0; - if (facing != 0 && facing != 1) facing = 1; + if (x < 0) + x = 0; + if (x >= MAP_SIZE) + x = MAP_SIZE - 1; + if (y < 0) + y = 0; + if (facing != 0 && facing != 1) + facing = 1; // Load inventory using new array format string personal_inv_str; @@ -1297,7 +1328,8 @@ bool load_game_state_from_file(const string&in filename) { reset_quick_slots(); string[] loadedQuickSlots = get_string_list_or_split(saveData, "equipment_quick_slots"); uint slot_count = loadedQuickSlots.length(); - if (slot_count > quick_slots.length()) slot_count = quick_slots.length(); + if (slot_count > quick_slots.length()) + slot_count = quick_slots.length(); for (uint i = 0; i < slot_count; i++) { int slot_value = parse_int(loadedQuickSlots[i]); if (slot_value >= EQUIP_NONE && slot_value <= EQUIP_FISHING_POLE) { @@ -1306,7 +1338,8 @@ bool load_game_state_from_file(const string&in filename) { } string[] loadedQuickSlotRunes = get_string_list_or_split(saveData, "equipment_quick_slot_runes"); uint rune_slot_count = loadedQuickSlotRunes.length(); - if (rune_slot_count > quick_slot_runes.length()) rune_slot_count = quick_slot_runes.length(); + if (rune_slot_count > quick_slot_runes.length()) + rune_slot_count = quick_slot_runes.length(); for (uint i = 0; i < rune_slot_count; i++) { int slot_value = parse_int(loadedQuickSlotRunes[i]); if (slot_value >= RUNE_NONE) { @@ -1316,7 +1349,8 @@ bool load_game_state_from_file(const string&in filename) { string[] loadedItemCountSlots = get_string_list_or_split(saveData, "item_count_slots"); uint count_slot_count = loadedItemCountSlots.length(); - if (count_slot_count > item_count_slots.length()) count_slot_count = item_count_slots.length(); + if (count_slot_count > item_count_slots.length()) + count_slot_count = item_count_slots.length(); for (uint i = 0; i < count_slot_count; i++) { int slot_value = parse_int(loadedItemCountSlots[i]); if (slot_value >= 0 && slot_value < ITEM_COUNT) { @@ -1369,14 +1403,21 @@ bool load_game_state_from_file(const string&in filename) { } // Validate equipped items now that runed items are loaded - if (!equipment_available(EQUIP_SPEAR)) spear_equipped = false; - if (!equipment_available(EQUIP_AXE)) axe_equipped = false; - if (!equipment_available(EQUIP_SLING)) sling_equipped = false; - if (!equipment_available(EQUIP_BOW)) bow_equipped = false; - if (!equipment_available(EQUIP_FISHING_POLE)) fishing_pole_equipped = false; + if (!equipment_available(EQUIP_SPEAR)) + spear_equipped = false; + if (!equipment_available(EQUIP_AXE)) + axe_equipped = false; + if (!equipment_available(EQUIP_SLING)) + sling_equipped = false; + if (!equipment_available(EQUIP_BOW)) + bow_equipped = false; + if (!equipment_available(EQUIP_FISHING_POLE)) + fishing_pole_equipped = false; - bool any_weapon_equipped = spear_equipped || axe_equipped || sling_equipped || bow_equipped || fishing_pole_equipped; - if (!any_weapon_equipped) equipped_weapon_rune = RUNE_NONE; + bool any_weapon_equipped = + spear_equipped || axe_equipped || sling_equipped || bow_equipped || fishing_pole_equipped; + if (!any_weapon_equipped) + equipped_weapon_rune = RUNE_NONE; if (!equipment_available(equipped_head)) { equipped_head = EQUIP_NONE; @@ -1438,13 +1479,20 @@ bool load_game_state_from_file(const string&in filename) { zombie_swarm_triggered_today = get_bool(saveData, "time_zombie_swarm_triggered_today", false); zombie_swarm_roll_done_today = get_bool(saveData, "time_zombie_swarm_roll_done_today", false); zombie_swarm_duration_hours = int(get_number(saveData, "time_zombie_swarm_duration_hours", 0)); - if (invasion_chance < 0) invasion_chance = 0; - if (invasion_chance > 100) invasion_chance = 100; - if (invasion_scheduled_hour < -1) invasion_scheduled_hour = -1; - if (invasion_scheduled_hour > 23) invasion_scheduled_hour = -1; - if (zombie_swarm_start_hour < -1 || zombie_swarm_start_hour > 23) zombie_swarm_start_hour = -1; - if (zombie_swarm_scheduled_hour < -1 || zombie_swarm_scheduled_hour > 23) zombie_swarm_scheduled_hour = -1; - if (zombie_swarm_duration_hours < 0) zombie_swarm_duration_hours = 0; + if (invasion_chance < 0) + invasion_chance = 0; + if (invasion_chance > 100) + invasion_chance = 100; + if (invasion_scheduled_hour < -1) + invasion_scheduled_hour = -1; + if (invasion_scheduled_hour > 23) + invasion_scheduled_hour = -1; + if (zombie_swarm_start_hour < -1 || zombie_swarm_start_hour > 23) + zombie_swarm_start_hour = -1; + if (zombie_swarm_scheduled_hour < -1 || zombie_swarm_scheduled_hour > 23) + zombie_swarm_scheduled_hour = -1; + if (zombie_swarm_duration_hours < 0) + zombie_swarm_duration_hours = 0; if (!zombie_swarm_active) { zombie_swarm_start_hour = -1; zombie_swarm_duration_hours = 0; @@ -1459,13 +1507,14 @@ bool load_game_state_from_file(const string&in filename) { if (playerItemBreakChance > PLAYER_ITEM_BREAK_CHANCE_MAX) { playerItemBreakChance = PLAYER_ITEM_BREAK_CHANCE_MAX; } - if (playerItemBreaksToday < 0) playerItemBreaksToday = 0; + if (playerItemBreaksToday < 0) + playerItemBreaksToday = 0; if (playerItemBreaksToday > PLAYER_ITEM_BREAKS_PER_DAY) { playerItemBreaksToday = PLAYER_ITEM_BREAKS_PER_DAY; } if (playerItemBreakPending) { - bool validPending = (playerItemBreakPendingType >= 0 && playerItemBreakPendingType < ITEM_COUNT) - || is_runed_item_type(playerItemBreakPendingType); + bool validPending = (playerItemBreakPendingType >= 0 && playerItemBreakPendingType < ITEM_COUNT) || + is_runed_item_type(playerItemBreakPendingType); if (!validPending) { playerItemBreakPending = false; playerItemBreakPendingType = -1; @@ -1481,18 +1530,24 @@ bool load_game_state_from_file(const string&in filename) { } else { wight_spawned_this_night_count = get_bool(saveData, "wight_spawned_this_night", false) ? 1 : 0; } - if (wight_spawned_this_night_count < 0) wight_spawned_this_night_count = 0; - if (wight_spawn_chance < WIGHT_SPAWN_CHANCE_START) wight_spawn_chance = WIGHT_SPAWN_CHANCE_START; - if (wight_spawn_chance > 100) wight_spawn_chance = 100; + if (wight_spawned_this_night_count < 0) + wight_spawned_this_night_count = 0; + if (wight_spawn_chance < WIGHT_SPAWN_CHANCE_START) + wight_spawn_chance = WIGHT_SPAWN_CHANCE_START; + if (wight_spawn_chance > 100) + wight_spawn_chance = 100; vampyr_spawn_chance = int(get_number(saveData, "vampyr_spawn_chance", VAMPYR_SPAWN_CHANCE_START)); if (has_number_key(saveData, "vampyr_spawned_this_night")) { vampyr_spawned_this_night_count = int(get_number(saveData, "vampyr_spawned_this_night", 0)); } else { vampyr_spawned_this_night_count = get_bool(saveData, "vampyr_spawned_this_night", false) ? 1 : 0; } - if (vampyr_spawned_this_night_count < 0) vampyr_spawned_this_night_count = 0; - if (vampyr_spawn_chance < VAMPYR_SPAWN_CHANCE_START) vampyr_spawn_chance = VAMPYR_SPAWN_CHANCE_START; - if (vampyr_spawn_chance > 100) vampyr_spawn_chance = 100; + if (vampyr_spawned_this_night_count < 0) + vampyr_spawned_this_night_count = 0; + if (vampyr_spawn_chance < VAMPYR_SPAWN_CHANCE_START) + vampyr_spawn_chance = VAMPYR_SPAWN_CHANCE_START; + if (vampyr_spawn_chance > 100) + vampyr_spawn_chance = 100; quest_queue.resize(0); string[] loadedQuests = get_string_list_or_split(saveData, "quest_queue"); @@ -1500,13 +1555,17 @@ bool load_game_state_from_file(const string&in filename) { int quest_type = parse_int(loadedQuests[i]); if (quest_type >= 0 && quest_type < QUEST_TYPE_COUNT) { quest_queue.insert_last(quest_type); - if (quest_queue.length() >= QUEST_MAX_ACTIVE) break; + if (quest_queue.length() >= QUEST_MAX_ACTIVE) + break; } } - if (current_hour < 0) current_hour = 0; - if (current_hour > 23) current_hour = 23; - if (current_day < 1) current_day = 1; + if (current_hour < 0) + current_hour = 0; + if (current_hour > 23) + current_hour = 23; + if (current_day < 1) + current_day = 1; is_daytime = (current_hour >= 6 && current_hour < 19); hour_timer.restart(); @@ -1520,11 +1579,12 @@ bool load_game_state_from_file(const string&in filename) { string[] treeData = get_string_list_or_split(saveData, "trees_data"); for (uint i = 0; i < treeData.length(); i++) { - string[]@ parts = treeData[i].split("|"); - if (parts.length() < 8) continue; + string[] @parts = treeData[i].split("|"); + if (parts.length() < 8) + continue; int pos = parse_int(parts[0]); - Tree@ tree = Tree(pos); + Tree @tree = Tree(pos); tree.sticks = parse_int(parts[1]); tree.vines = parse_int(parts[2]); tree.health = parse_int(parts[3]); @@ -1539,11 +1599,12 @@ bool load_game_state_from_file(const string&in filename) { string[] snareData = get_string_list_or_split(saveData, "snares_data"); for (uint i = 0; i < snareData.length(); i++) { - string[]@ parts = snareData[i].split("|"); - if (parts.length() < 6) continue; + string[] @parts = snareData[i].split("|"); + if (parts.length() < 6) + continue; int pos = parse_int(parts[0]); - WorldSnare@ snare = WorldSnare(pos); + WorldSnare @snare = WorldSnare(pos); snare.has_catch = (parse_int(parts[1]) == 1); snare.catch_type = parts[2]; snare.catch_chance = parse_int(parts[3]); @@ -1559,11 +1620,12 @@ bool load_game_state_from_file(const string&in filename) { string[] fireData = get_string_list_or_split(saveData, "fires_data"); for (uint i = 0; i < fireData.length(); i++) { - string[]@ parts = fireData[i].split("|"); - if (parts.length() < 3) continue; + string[] @parts = fireData[i].split("|"); + if (parts.length() < 3) + continue; int pos = parse_int(parts[0]); - WorldFire@ fire = WorldFire(pos); + WorldFire @fire = WorldFire(pos); fire.fuel_remaining = parse_int(parts[1]); fire.low_fuel_warned = (parse_int(parts[2]) == 1); fire.fuel_timer.restart(); @@ -1609,19 +1671,22 @@ bool load_game_state_from_file(const string&in filename) { string[] streamData = get_string_list_or_split(saveData, "streams_data"); for (uint i = 0; i < streamData.length(); i++) { - string[]@ parts = streamData[i].split("|"); - if (parts.length() < 2) continue; + string[] @parts = streamData[i].split("|"); + if (parts.length() < 2) + continue; int startPos = parse_int(parts[0]); int width = parse_int(parts[1]); - if (width < 1) width = 1; + if (width < 1) + width = 1; add_world_stream(startPos, width); } string[] banditData = get_string_list_or_split(saveData, "bandits_data"); for (uint i = 0; i < banditData.length(); i++) { - string[]@ parts = banditData[i].split("|"); - if (parts.length() < 6) continue; + string[] @parts = banditData[i].split("|"); + if (parts.length() < 6) + continue; int pos = parse_int(parts[0]); int health = parse_int(parts[1]); @@ -1634,7 +1699,8 @@ bool load_game_state_from_file(const string&in filename) { int home_end = pos; if (parts.length() >= 7) { invader_type = parts[6]; - if (invader_type == "") invader_type = "bandit"; + if (invader_type == "") + invader_type = "bandit"; } if (parts.length() >= 9) { home_start = parse_int(parts[7]); @@ -1651,7 +1717,7 @@ bool load_game_state_from_file(const string&in filename) { } // Create bandit with dummy expansion area (position will be overridden) - Bandit@ b = Bandit(pos, home_start, home_end, invader_type); + Bandit @b = Bandit(pos, home_start, home_end, invader_type); b.position = pos; b.health = health; b.weapon_type = weapon; @@ -1659,7 +1725,8 @@ bool load_game_state_from_file(const string&in filename) { b.wander_direction = wander_dir; b.move_interval = move_int; b.invader_type = invader_type; - b.wander_direction_change_interval = random(BANDIT_WANDER_DIRECTION_CHANGE_MIN, BANDIT_WANDER_DIRECTION_CHANGE_MAX); + b.wander_direction_change_interval = + random(BANDIT_WANDER_DIRECTION_CHANGE_MIN, BANDIT_WANDER_DIRECTION_CHANGE_MAX); b.wander_direction_timer.restart(); b.move_timer.restart(); b.attack_timer.restart(); @@ -1679,27 +1746,28 @@ bool load_game_state_from_file(const string&in filename) { string[] mountainData = get_string_list_or_split(saveData, "mountains_data"); for (uint i = 0; i < mountainData.length(); i++) { - string[]@ parts = mountainData[i].split("|"); - if (parts.length() < 5) continue; + string[] @parts = mountainData[i].split("|"); + if (parts.length() < 5) + continue; int start_pos = parse_int(parts[0]); int end_pos = parse_int(parts[1]); int size = end_pos - start_pos + 1; // Create mountain with minimal init (we'll override everything) - MountainRange@ mountain = MountainRange(start_pos, 1); + MountainRange @mountain = MountainRange(start_pos, 1); mountain.start_position = start_pos; mountain.end_position = end_pos; // Parse elevations - string[]@ elev_parts = parts[2].split(","); + string[] @elev_parts = parts[2].split(","); mountain.elevations.resize(elev_parts.length()); for (uint j = 0; j < elev_parts.length(); j++) { mountain.elevations[j] = parse_int(elev_parts[j]); } // Parse terrain types - string[]@ terrain_parts = parts[3].split(","); + string[] @terrain_parts = parts[3].split(","); mountain.terrain_types.resize(terrain_parts.length()); for (uint j = 0; j < terrain_parts.length(); j++) { mountain.terrain_types[j] = terrain_parts[j]; @@ -1707,7 +1775,7 @@ bool load_game_state_from_file(const string&in filename) { // Parse stream positions if (parts[4].length() > 0) { - string[]@ stream_parts = parts[4].split(","); + string[] @stream_parts = parts[4].split(","); for (uint j = 0; j < stream_parts.length(); j++) { mountain.stream_positions.insert_last(parse_int(stream_parts[j])); } @@ -1718,8 +1786,9 @@ bool load_game_state_from_file(const string&in filename) { string[] dropData = get_string_list_or_split(saveData, "drops_data"); for (uint i = 0; i < dropData.length(); i++) { - string[]@ parts = dropData[i].split("|"); - if (parts.length() < 2) continue; + string[] @parts = dropData[i].split("|"); + if (parts.length() < 2) + continue; add_world_drop(parse_int(parts[0]), parts[1]); } diff --git a/src/sound_settings.nvgt b/src/sound_settings.nvgt new file mode 100644 index 0000000..a8d8ed4 --- /dev/null +++ b/src/sound_settings.nvgt @@ -0,0 +1,49 @@ +// Project-local learn sounds configuration. +// Keep this file in src/ so libstorm-nvgt can stay untouched. + +void configure_project_learn_sounds() { + learn_sounds_reset_configuration(); + + // Skip entries can be full file paths or directories ending with "/". + learn_sounds_add_skip_entry("sounds/quests/bone1.ogg"); + learn_sounds_add_skip_entry("sounds/quests/bone2.ogg"); + learn_sounds_add_skip_entry("sounds/quests/bone3.ogg"); + learn_sounds_add_skip_entry("sounds/quests/bone4.ogg"); + learn_sounds_add_skip_entry("sounds/quests/bone5.ogg"); + learn_sounds_add_skip_entry("sounds/quests/bone6.ogg"); + learn_sounds_add_skip_entry("sounds/quests/bone7.ogg"); + learn_sounds_add_skip_entry("sounds/quests/bone8.ogg"); + learn_sounds_add_skip_entry("sounds/actions/fishpole.ogg"); + learn_sounds_add_skip_entry("sounds/actions/hit_ground.ogg"); + learn_sounds_add_skip_entry("sounds/menu/"); + learn_sounds_add_skip_entry("sounds/nature/"); + learn_sounds_add_skip_entry("sounds/pets/"); + learn_sounds_add_skip_entry("sounds/quests/"); + + learn_sounds_add_description( + "sounds/actions/bad_cast.ogg", + i18n_text("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", + i18n_text("When casting release control when over water. When catching release when sound is over player.")); + learn_sounds_add_description("sounds/enemies/enter_range.ogg", + i18n_text("An enemy is in range of your currently wielded weapon.")); + learn_sounds_add_description("sounds/enemies/exit_range.ogg", + i18n_text("An enemy is no longer in range of your currently wielded weapon.")); + learn_sounds_add_description("sounds/actions/falling.ogg", i18n_text("Lowers in pitch as the fall progresses.")); + learn_sounds_add_description("sounds/enemies/invasion.ogg", i18n_text("The war drums pound! Prepare for combat!")); + learn_sounds_add_description("sounds/items/miscellaneous.ogg", + i18n_text("You picked up an item for which there is no specific sound.")); + learn_sounds_add_description("sounds/enemies/goblin1.ogg", i18n_text("Inhabits mountainous regions.")); + learn_sounds_add_description("sounds/enemies/undead_resident1.ogg", + i18n_text("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", + i18n_text("More powerful undead. They do not lose track of you when you are in the base.")); + learn_sounds_add_description("sounds/enemies/vampyr1.ogg", i18n_text("Ever hungry, they feed upon your residents.")); + learn_sounds_add_description("sounds/enemies/vampyr2.ogg", i18n_text("Ever hungry, they feed upon your residents.")); + learn_sounds_add_description("sounds/enemies/vampyr3.ogg", i18n_text("Ever hungry, they feed upon your residents.")); + learn_sounds_add_description("sounds/enemies/vampyr4.ogg", i18n_text("Ever hungry, they feed upon your residents.")); + learn_sounds_add_description("sounds/enemies/yeti1.ogg", i18n_text("Inhabits snowy regions.")); + learn_sounds_add_description("sounds/enemies/zombie1.ogg", i18n_text("Basic undead.")); +} diff --git a/src/terrain_lookup.nvgt b/src/terrain_lookup.nvgt index 86d1e56..9da6ca9 100644 --- a/src/terrain_lookup.nvgt +++ b/src/terrain_lookup.nvgt @@ -4,14 +4,17 @@ string get_terrain_at_position(int posX) { return "water"; } - MountainRange@ mountain = get_mountain_at(posX); + MountainRange @mountain = get_mountain_at(posX); if (mountain !is null) { return mountain.get_terrain_at(posX); } - if (posX <= BASE_END) return "wood"; - if (posX <= GRASS_END) return "grass"; - if (posX <= GRAVEL_END) return "gravel"; + if (posX <= BASE_END) + return "wood"; + if (posX <= GRASS_END) + return "grass"; + if (posX <= GRAVEL_END) + return "gravel"; int index = posX - expanded_area_start; if (index >= 0 && index < int(expanded_terrain_types.length())) { 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/time_system.nvgt b/src/time_system.nvgt index a56ccbf..231fe98 100644 --- a/src/time_system.nvgt +++ b/src/time_system.nvgt @@ -3,7 +3,7 @@ const int MS_PER_HOUR = 60000; int current_hour = 8; // Start at 8 AM -int current_day = 1; // Track current day +int current_day = 1; // Track current day timer hour_timer; int day_sound_handle = -1; @@ -24,9 +24,9 @@ bool playerItemBreakSoundPending = false; bool crossfade_active = false; bool crossfade_to_night = false; // true = fading to night, false = fading to day timer crossfade_timer; -const int CROSSFADE_DURATION = 60000; // 1 minute (1 game hour) +const int CROSSFADE_DURATION = 60000; // 1 minute (1 game hour) const float CROSSFADE_MIN_VOLUME = -25.0; // dB, keep overlap audible during crossfade -const float CROSSFADE_MAX_VOLUME = 0.0; // dB, full volume +const float CROSSFADE_MAX_VOLUME = 0.0; // dB, full volume // Expansion and invasion tracking bool area_expanded_today = false; @@ -95,15 +95,17 @@ void reset_player_item_break_state() { reset_player_item_break_audio_state(); } -void queue_player_item_break_message(const string&in message) { - if (message.length() == 0) return; +void queue_player_item_break_message(const string& in message) { + if (message.length() == 0) + return; playerItemBreakMessage = message; playerItemBreakSoundHandle = p.play_stationary("sounds/items/item_breaks.ogg", false); playerItemBreakSoundPending = true; } void update_player_item_break_message() { - if (!playerItemBreakSoundPending) return; + if (!playerItemBreakSoundPending) + return; if (playerItemBreakSoundHandle != -1 && p.sound_is_active(playerItemBreakSoundHandle)) { return; } @@ -116,8 +118,10 @@ void update_player_item_break_message() { } void resolve_pending_player_item_break() { - if (!playerItemBreakPending) return; - if (x > BASE_END) return; + if (!playerItemBreakPending) + return; + if (x > BASE_END) + return; if (playerItemBreakPendingType == -1) { playerItemBreakPending = false; return; @@ -132,12 +136,15 @@ void resolve_pending_player_item_break() { } void attempt_player_item_break_check() { - if (playerItemBreakPending) return; - if (playerItemBreaksToday >= PLAYER_ITEM_BREAKS_PER_DAY) return; + if (playerItemBreakPending) + return; + if (playerItemBreaksToday >= PLAYER_ITEM_BREAKS_PER_DAY) + return; int[] breakableItems; get_breakable_personal_item_types(breakableItems); - if (breakableItems.length() == 0) return; + if (breakableItems.length() == 0) + return; int roll = random(1, PLAYER_ITEM_BREAK_ROLL_MAX); int checkChance = int(playerItemBreakChance); // Floor for comparison @@ -167,7 +174,7 @@ void attempt_player_item_break_check() { } } -string get_invasion_enemy_type_for_terrain(const string&in terrain_type) { +string get_invasion_enemy_type_for_terrain(const string& in terrain_type) { for (uint i = 0; i < invasion_terrain_enemy_map.length(); i++) { string entry = invasion_terrain_enemy_map[i]; int eq_pos = entry.find("="); @@ -184,12 +191,13 @@ string get_invasion_enemy_type_for_terrain(const string&in terrain_type) { return default_invasion_enemy_type; } -string get_invasion_enemy_label(const string&in enemy_type) { - if (enemy_type != "") return enemy_type; +string get_invasion_enemy_label(const string& in enemy_type) { + if (enemy_type != "") + return enemy_type; return default_invasion_enemy_type; } -string get_invasion_enemy_plural(const string&in enemy_type) { +string get_invasion_enemy_plural(const string& in enemy_type) { return get_invasion_enemy_label(enemy_type) + "s"; } @@ -257,14 +265,21 @@ string expand_regular_area() { // Fill the new area up to its tree cap with proper spacing spawn_trees(new_start, new_end); } else { - int stream_width = random(1, 5); - int stream_start = random(0, EXPANSION_SIZE - stream_width); - int actual_start = new_start + stream_start; - add_world_stream(actual_start, stream_width); + // Keep both extreme edge tiles dry so area transitions are never blocked. + int max_stream_width = EXPANSION_SIZE - 2; + if (max_stream_width > 0) { + int stream_width = random(1, 5); + if (stream_width > max_stream_width) { + stream_width = max_stream_width; + } + int stream_start = random(1, EXPANSION_SIZE - stream_width - 1); + int actual_start = new_start + stream_start; + add_world_stream(actual_start, stream_width); + } } area_expanded_today = true; - notify("The area has expanded! New territory discovered to the east."); + notify(tr("system.time.event.area_expanded")); return terrain_type; } @@ -280,7 +295,7 @@ void expand_mountain() { MAP_SIZE += size; // Generate mountain range - MountainRange@ mountain = MountainRange(new_start, size); + MountainRange @mountain = MountainRange(new_start, size); world_mountains.insert_last(mountain); // Fill terrain types array for compatibility with save system @@ -319,7 +334,7 @@ void expand_mountain() { } area_expanded_today = true; - notify("A mountain range has been discovered to the east!"); + notify(tr("system.time.event.mountain_discovered")); } void start_invasion() { @@ -328,9 +343,14 @@ void start_invasion() { invasion_start_hour = current_hour; invasion_enemy_type = get_invasion_enemy_type_for_terrain(expansion_terrain); invasion_started_once = true; - string source = (expansion_terrain == "mountain") ? "the mountains" : "the new area"; + string source = + (expansion_terrain == "mountain") ? tr("system.time.event.invasion_source_mountains") + : tr("system.time.event.invasion_source_new_area"); string enemy_plural = get_invasion_enemy_plural(invasion_enemy_type); - notify(enemy_plural + " are invading from " + source + "!"); + dictionary args; + args.set("enemy_plural", enemy_plural); + args.set("source", source); + notify(trf("system.time.event.invasion_start", args)); } void update_invasion_chance_for_new_day() { @@ -343,14 +363,17 @@ void update_invasion_chance_for_new_day() { invasion_chance = 25; } else { invasion_chance += 25; - if (invasion_chance > 100) invasion_chance = 100; + if (invasion_chance > 100) + invasion_chance = 100; } } } int get_random_invasion_hour(int min_hour) { - if (min_hour < 6) min_hour = 6; - if (min_hour > 11) return -1; + if (min_hour < 6) + min_hour = 6; + if (min_hour > 11) + return -1; return random(min_hour, 11); } @@ -359,7 +382,8 @@ bool is_night_hour(int hour) { } int get_random_zombie_swarm_hour(int min_hour) { - if (!is_night_hour(min_hour)) return -1; + if (!is_night_hour(min_hour)) + return -1; if (min_hour >= 19) { return random(min_hour, 23); } @@ -367,14 +391,17 @@ int get_random_zombie_swarm_hour(int min_hour) { } void schedule_invasion() { - if (invasion_scheduled_hour != -1) return; + if (invasion_scheduled_hour != -1) + return; int hour = get_random_invasion_hour(current_hour); - if (hour == -1) return; + if (hour == -1) + return; invasion_scheduled_hour = hour; } void check_scheduled_invasion() { - if (invasion_active) return; + if (invasion_active) + return; // Check scheduled invasion regardless of triggered flag (fixes bug where flag was set early in old saves) if (invasion_scheduled_hour != -1) { @@ -388,19 +415,25 @@ void check_scheduled_invasion() { return; } - if (invasion_triggered_today) return; + if (invasion_triggered_today) + return; } void attempt_daily_invasion() { - if (current_day < 2) return; - if (invasion_triggered_today || invasion_active) return; - if (invasion_roll_done_today) return; - if (current_hour < 6 || current_hour > 12) return; + if (current_day < 2) + return; + if (invasion_triggered_today || invasion_active) + return; + if (invasion_roll_done_today) + return; + if (current_hour < 6 || current_hour > 12) + return; invasion_roll_done_today = true; int roll = random(1, 100); - if (roll > invasion_chance) return; + if (roll > invasion_chance) + return; schedule_invasion(); check_scheduled_invasion(); @@ -412,20 +445,25 @@ bool can_roll_zombie_swarm_today() { int get_zombie_swarm_duration_hours() { int offset = current_day - ZOMBIE_SWARM_START_DAY; - if (offset < 0) offset = 0; + if (offset < 0) + offset = 0; int cycles = offset / ZOMBIE_SWARM_INTERVAL_DAYS; int duration = ZOMBIE_SWARM_BASE_DURATION_HOURS + (cycles * ZOMBIE_SWARM_DURATION_STEP_HOURS); - if (duration < 1) duration = 1; + if (duration < 1) + duration = 1; return duration; } int get_zombie_swarm_chance_for_day() { int offset = current_day - ZOMBIE_SWARM_START_DAY; - if (offset < 0) offset = 0; + if (offset < 0) + offset = 0; int cycles = offset / ZOMBIE_SWARM_CHANCE_INTERVAL_DAYS; int chance = ZOMBIE_SWARM_CHANCE_START + (cycles * ZOMBIE_SWARM_CHANCE_STEP); - if (chance > ZOMBIE_SWARM_CHANCE_CAP) chance = ZOMBIE_SWARM_CHANCE_CAP; - if (chance < 0) chance = 0; + if (chance > ZOMBIE_SWARM_CHANCE_CAP) + chance = ZOMBIE_SWARM_CHANCE_CAP; + if (chance < 0) + chance = 0; return chance; } @@ -433,7 +471,7 @@ void start_zombie_swarm() { zombie_swarm_active = true; zombie_swarm_start_hour = current_hour; zombie_swarm_duration_hours = get_zombie_swarm_duration_hours(); - speak_with_history("A swarm of zombies has been spotted.", true); + speak_with_history(tr("system.time.event.zombie_swarm_spotted"), true); } void end_zombie_swarm() { @@ -443,7 +481,8 @@ void end_zombie_swarm() { } void check_zombie_swarm_status() { - if (!zombie_swarm_active) return; + if (!zombie_swarm_active) + return; if (!is_night_hour(current_hour)) { end_zombie_swarm(); return; @@ -458,14 +497,17 @@ void check_zombie_swarm_status() { } void schedule_zombie_swarm() { - if (zombie_swarm_scheduled_hour != -1) return; + if (zombie_swarm_scheduled_hour != -1) + return; int hour = get_random_zombie_swarm_hour(current_hour); - if (hour == -1) return; + if (hour == -1) + return; zombie_swarm_scheduled_hour = hour; } void check_scheduled_zombie_swarm() { - if (zombie_swarm_active) return; + if (zombie_swarm_active) + return; if (zombie_swarm_scheduled_hour != -1) { if (!is_night_hour(zombie_swarm_scheduled_hour) || !is_night_hour(current_hour)) { zombie_swarm_scheduled_hour = -1; @@ -478,13 +520,17 @@ void check_scheduled_zombie_swarm() { } return; } - if (zombie_swarm_triggered_today) return; + if (zombie_swarm_triggered_today) + return; } void attempt_daily_zombie_swarm() { - if (!can_roll_zombie_swarm_today()) return; - if (zombie_swarm_roll_done_today || zombie_swarm_triggered_today || zombie_swarm_active) return; - if (!is_night_hour(current_hour)) return; + if (!can_roll_zombie_swarm_today()) + return; + if (zombie_swarm_roll_done_today || zombie_swarm_triggered_today || zombie_swarm_active) + return; + if (!is_night_hour(current_hour)) + return; zombie_swarm_roll_done_today = true; int chance = get_zombie_swarm_chance_for_day(); @@ -503,13 +549,15 @@ void attempt_daily_zombie_swarm() { check_scheduled_zombie_swarm(); } -void get_expanded_area_segments(int[]@ areaStarts, int[]@ areaEnds, string[]@ areaTypes) { +void get_expanded_area_segments(int[] @areaStarts, int[] @areaEnds, string[] @areaTypes) { areaStarts.resize(0); areaEnds.resize(0); areaTypes.resize(0); - if (expanded_area_start == -1) return; + if (expanded_area_start == -1) + return; int total = int(expanded_terrain_types.length()); - if (total <= 0) return; + if (total <= 0) + return; string current_type = get_expanded_area_type(0); int segment_start = 0; @@ -530,21 +578,28 @@ void get_expanded_area_segments(int[]@ areaStarts, int[]@ areaEnds, string[]@ ar } void attempt_expansion_roamer_spawn() { - if (!is_daytime) return; - if (invasion_active) return; - if (!invasion_started_once) return; - if (expanded_area_start == -1) return; - if (current_hour % EXPANSION_ROAMER_SPAWN_INTERVAL_HOURS != 0) return; + if (!is_daytime) + return; + if (invasion_active) + return; + if (!invasion_started_once) + return; + if (expanded_area_start == -1) + return; + if (current_hour % EXPANSION_ROAMER_SPAWN_INTERVAL_HOURS != 0) + return; int[] areaStarts; int[] areaEnds; string[] areaTypes; get_expanded_area_segments(areaStarts, areaEnds, areaTypes); - if (areaStarts.length() == 0) return; + if (areaStarts.length() == 0) + return; for (uint i = 0; i < areaStarts.length(); i++) { int count = count_bandits_in_range(areaStarts[i], areaEnds[i]); - if (count >= EXPANSION_ROAMER_MAX_PER_AREA) continue; + if (count >= EXPANSION_ROAMER_MAX_PER_AREA) + continue; string invader_type = get_invasion_enemy_type_for_terrain(areaTypes[i]); spawn_bandit(areaStarts[i], areaEnds[i], invader_type); } @@ -588,15 +643,23 @@ void attempt_resident_recruitment() { added = MAX_RESIDENTS - residents_count; } - if (added <= 0) return; + if (added <= 0) + return; residents_count += added; - string join_message = (added == 1) ? "A survivor joins your base." : "" + added + " survivors join your base."; + string join_message = ""; + if (added == 1) { + join_message = tr("system.time.event.survivor_joins_one"); + } else { + dictionary joinArgs; + joinArgs.set("count", added); + join_message = trf("system.time.event.survivor_joins_many", joinArgs); + } notify(join_message); // Notify if base is now full if (residents_count >= MAX_RESIDENTS) { - notify("Your base is at maximum capacity."); + notify(tr("system.time.event.base_max_capacity")); } } @@ -604,7 +667,9 @@ void end_invasion() { invasion_active = false; invasion_start_hour = -1; transition_bandits_to_wandering(); - notify("The " + get_invasion_enemy_label(invasion_enemy_type) + " invasion has ended."); + dictionary args; + args.set("enemy", get_invasion_enemy_label(invasion_enemy_type)); + notify(trf("system.time.event.invasion_ended", args)); attempt_resident_recruitment(); } @@ -617,13 +682,15 @@ void transition_bandits_to_wandering() { } bandits[i].behavior_state = "wandering"; bandits[i].wander_direction = random(-1, 1); - bandits[i].wander_direction_change_interval = random(BANDIT_WANDER_DIRECTION_CHANGE_MIN, BANDIT_WANDER_DIRECTION_CHANGE_MAX); + bandits[i].wander_direction_change_interval = + random(BANDIT_WANDER_DIRECTION_CHANGE_MIN, BANDIT_WANDER_DIRECTION_CHANGE_MAX); bandits[i].wander_direction_timer.restart(); } } void check_invasion_status() { - if (!invasion_active) return; + if (!invasion_active) + return; // Check if invasion duration has elapsed int hours_elapsed = current_hour - invasion_start_hour; @@ -656,45 +723,53 @@ void update_blessings() { if (blessing_speed_active && blessing_speed_timer.elapsed >= BLESSING_SPEED_DURATION) { blessing_speed_active = false; update_max_health_from_equipment(); - speak_with_history("The speed blessing fades.", true); + speak_with_history(tr("system.time.event.blessing_speed_fades"), true); } if (blessing_resident_active && blessing_resident_timer.elapsed >= BLESSING_RESIDENT_DURATION) { blessing_resident_active = false; - speak_with_history("The residents' purpose fades.", true); + speak_with_history(tr("system.time.event.blessing_resident_fades"), true); } if (blessing_search_active && blessing_search_timer.elapsed >= BLESSING_SEARCH_DURATION) { blessing_search_active = false; - speak_with_history("The eagle's sight fades.", true); + speak_with_history(tr("system.time.event.blessing_search_fades"), true); } } void attempt_blessing() { - if (favor < 1.0) return; + if (favor < 1.0) + return; int roll = random(1, 100); - if (roll > BLESSING_TRIGGER_CHANCE) return; + if (roll > BLESSING_TRIGGER_CHANCE) + return; int[] options; - if (player_health < max_health) options.insert_last(0); - if (!blessing_speed_active) options.insert_last(1); - if (barricade_health < BARRICADE_MAX_HEALTH) options.insert_last(2); - if (residents_count > 0 && !blessing_resident_active) options.insert_last(3); - if (!blessing_search_active) options.insert_last(4); - if (options.length() == 0) return; + if (player_health < max_health) + options.insert_last(0); + if (!blessing_speed_active) + options.insert_last(1); + if (barricade_health < BARRICADE_MAX_HEALTH) + options.insert_last(2); + if (residents_count > 0 && !blessing_resident_active) + options.insert_last(3); + if (!blessing_search_active) + options.insert_last(4); + if (options.length() == 0) + return; int choice = options[random(0, options.length() - 1)]; favor -= 1.0; - if (favor < 0) favor = 0; + if (favor < 0) + favor = 0; - string[] god_names = { - "Odin's", "Thor's", "Freyja's", "Loki's", "Tyr's", "Baldur's", - "Frigg's", "Heimdall's", "Hel's", "Fenrir's", "Freyr's", "The gods'" - }; + string[] god_names = {"Odin's", "Thor's", "Freyja's", "Loki's", "Tyr's", "Baldur's", + "Frigg's", "Heimdall's", "Hel's", "Fenrir's", "Freyr's", "The gods'"}; string god_name = god_names[random(0, god_names.length() - 1)]; if (choice == 0) { int before = player_health; player_health += BLESSING_HEAL_AMOUNT; - if (player_health > max_health) player_health = max_health; + if (player_health > max_health) + player_health = max_health; int healed = player_health - before; string bonus = (healed > 0) ? "You feel restored. +" + healed + " health." : "You feel restored."; notify(god_name + " favor shines upon you. " + bonus); @@ -705,9 +780,8 @@ void attempt_blessing() { notify(god_name + " favor shines upon you. You feel swift for a while."); } else if (choice == 2) { int gained = add_barricade_health(BLESSING_BARRICADE_REPAIR); - string bonus = (gained > 0) - ? "A divine force repairs the barricade. +" + gained + " health." - : "A divine force surrounds the barricade."; + string bonus = (gained > 0) ? "A divine force repairs the barricade. +" + gained + " health." + : "A divine force surrounds the barricade."; notify(god_name + " favor shines upon you. " + bonus); } else if (choice == 3) { blessing_resident_active = true; @@ -722,12 +796,17 @@ void attempt_blessing() { bool should_attempt_resident_foraging(int hour) { int residentCount = residents_count; - if (residentCount <= 0) return false; + if (residentCount <= 0) + return false; - if (hour == 6) return true; - if (residentCount == 1) return false; - if (residentCount == 2) return hour == 12; - if (residentCount == 3) return hour == 12 || hour == 18; + if (hour == 6) + return true; + if (residentCount == 1) + return false; + if (residentCount == 2) + return hour == 12; + if (residentCount == 3) + return hour == 12 || hour == 18; // 4+ residents: spread across daytime hours return hour == 10 || hour == 14 || hour == 18; @@ -762,13 +841,13 @@ void update_time() { handle_pet_hourly_update(current_hour); if (current_hour == 18 && !sun_setting_warned) { - notify("The sun is setting."); + notify(tr("system.time.event.sun_setting")); sun_setting_warned = true; } else if (current_hour == 19) { sun_setting_warned = false; } if (current_hour == 5 && !sunrise_warned) { - notify("The sky begins to brighten."); + notify(tr("system.time.event.sky_brightening")); sunrise_warned = true; } else if (current_hour == 6) { sunrise_warned = false; @@ -794,13 +873,16 @@ void update_time() { if (current_hour >= day_start_hour && current_hour < day_end_hour) { int repair_window_hours = day_end_hour - day_start_hour; int interval = repair_window_hours / residents_count; - if (interval < 1) interval = 1; + if (interval < 1) + interval = 1; int day_hour = current_hour - day_start_hour; if (day_hour % interval == 0) { if (has_any_storage_food()) { int gained = add_barricade_health(residents_count * get_resident_effect_multiplier()); if (gained > 0 && x <= BASE_END) { - speak_with_history("Residents repaired the barricade. +" + gained + " health.", true); + dictionary args; + args.set("health", gained); + speak_with_history(trf("system.time.event.residents_repaired_barricade", args), true); } } } @@ -854,7 +936,8 @@ void update_time() { } void update_incense_burning() { - if (!incense_burning || incense_hours_remaining <= 0) return; + if (!incense_burning || incense_hours_remaining <= 0) + return; favor += INCENSE_FAVOR_PER_HOUR; incense_hours_remaining--; @@ -862,7 +945,7 @@ void update_incense_burning() { if (incense_hours_remaining <= 0) { incense_hours_remaining = 0; incense_burning = false; - notify("The incense has burned out."); + notify(tr("system.time.event.incense_burned_out")); } } @@ -874,46 +957,57 @@ void check_time_input() { string get_time_string() { int display_hour = current_hour; - string period = "am"; - + string periodKey = "system.time.period.am"; + if (display_hour == 0) { display_hour = 12; } else if (display_hour == 12) { - period = "pm"; + periodKey = "system.time.period.pm"; } else if (display_hour > 12) { display_hour -= 12; - period = "pm"; + periodKey = "system.time.period.pm"; } - - string result = display_hour + " oclock " + period + " day " + current_day; + + dictionary timeArgs; + timeArgs.set("hour", display_hour); + timeArgs.set("period", tr(periodKey)); + timeArgs.set("day", current_day); + string result = trf("system.time.report", timeArgs); + string[] conditions; if (is_daytime) { if (weather_state == WEATHER_CLEAR) { - conditions.insert_last("Sunny"); + conditions.insert_last(tr("system.time.condition.sunny")); } else { - conditions.insert_last("Daylight"); + conditions.insert_last(tr("system.time.condition.daylight")); } } else { - conditions.insert_last("Dark"); + conditions.insert_last(tr("system.time.condition.dark")); } if (weather_state == WEATHER_WINDY) { - conditions.insert_last("Windy"); + conditions.insert_last(tr("system.time.condition.windy")); } else if (weather_state == WEATHER_RAINY) { - conditions.insert_last("Raining"); + conditions.insert_last(tr("system.time.condition.raining")); } else if (weather_state == WEATHER_STORMY) { - conditions.insert_last("Storming"); + conditions.insert_last(tr("system.time.condition.storming")); } - bool can_see_night_sky = (weather_state == WEATHER_CLEAR || weather_state == WEATHER_WINDY); - if (!is_daytime && can_see_night_sky) { - conditions.insert_last("Moon and stars"); + bool canSeeNightSky = (weather_state == WEATHER_CLEAR || weather_state == WEATHER_WINDY); + if (!is_daytime && canSeeNightSky) { + conditions.insert_last(tr("system.time.condition.moon_and_stars")); } + if (conditions.length() > 0) { - string condition_text = conditions[0]; + string conditionText = conditions[0]; for (uint i = 1; i < conditions.length(); i++) { - condition_text += ", " + conditions[i]; + conditionText += ", " + conditions[i]; } - result += ". " + condition_text; + + dictionary conditionArgs; + conditionArgs.set("time", result); + conditionArgs.set("conditions", conditionText); + result = trf("system.time.report_with_conditions", conditionArgs); } + return result; } @@ -964,7 +1058,8 @@ void start_crossfade(bool to_night) { } void update_crossfade() { - if (!crossfade_active) return; + if (!crossfade_active) + return; // If a handle went inactive mid-fade, cancel and restart ambience to avoid fading unrelated sounds. if (crossfade_to_night) { @@ -996,22 +1091,27 @@ void update_crossfade() { } float progress = float(crossfade_timer.elapsed) / float(CROSSFADE_DURATION); - if (progress > 1.0) progress = 1.0; + if (progress > 1.0) + progress = 1.0; // Volume interpolation: use a slow-start curve to make fade-outs more gradual float volume_range = CROSSFADE_MAX_VOLUME - CROSSFADE_MIN_VOLUME; // dB range float eased_progress = progress * progress; float fade_out_vol = CROSSFADE_MAX_VOLUME - (volume_range * eased_progress); // 0 -> min - float fade_in_vol = CROSSFADE_MIN_VOLUME + (volume_range * eased_progress); // min -> 0 + float fade_in_vol = CROSSFADE_MIN_VOLUME + (volume_range * eased_progress); // min -> 0 if (crossfade_to_night) { // Fading day out, night in - if (day_sound_handle != -1) p.update_sound_start_values(day_sound_handle, 0.0, fade_out_vol, 1.0); - if (night_sound_handle != -1) p.update_sound_start_values(night_sound_handle, 0.0, fade_in_vol, 1.0); + if (day_sound_handle != -1) + p.update_sound_start_values(day_sound_handle, 0.0, fade_out_vol, 1.0); + if (night_sound_handle != -1) + p.update_sound_start_values(night_sound_handle, 0.0, fade_in_vol, 1.0); } else { // Fading night out, day in - if (night_sound_handle != -1) p.update_sound_start_values(night_sound_handle, 0.0, fade_out_vol, 1.0); - if (day_sound_handle != -1) p.update_sound_start_values(day_sound_handle, 0.0, fade_in_vol, 1.0); + if (night_sound_handle != -1) + p.update_sound_start_values(night_sound_handle, 0.0, fade_out_vol, 1.0); + if (day_sound_handle != -1) + p.update_sound_start_values(day_sound_handle, 0.0, fade_in_vol, 1.0); } // Complete the crossfade @@ -1029,7 +1129,8 @@ void complete_crossfade() { p.destroy_sound(day_sound_handle); day_sound_handle = -1; } - if (night_sound_handle != -1) p.update_sound_start_values(night_sound_handle, 0.0, 0.0, 1.0); + if (night_sound_handle != -1) + p.update_sound_start_values(night_sound_handle, 0.0, 0.0, 1.0); is_daytime = false; if (night_sound_handle == -1 || !p.sound_is_active(night_sound_handle)) { update_ambience(false); @@ -1040,7 +1141,8 @@ void complete_crossfade() { p.destroy_sound(night_sound_handle); night_sound_handle = -1; } - if (day_sound_handle != -1) p.update_sound_start_values(day_sound_handle, 0.0, 0.0, 1.0); + if (day_sound_handle != -1) + p.update_sound_start_values(day_sound_handle, 0.0, 0.0, 1.0); is_daytime = true; if (day_sound_handle == -1 || !p.sound_is_active(day_sound_handle)) { update_ambience(false); @@ -1049,7 +1151,8 @@ void complete_crossfade() { } void ensure_ambience_running() { - if (crossfade_active) return; + if (crossfade_active) + return; if (is_daytime) { if (day_sound_handle == -1 || !p.sound_is_active(day_sound_handle)) { diff --git a/src/weapon_range_audio.nvgt b/src/weapon_range_audio.nvgt index 2d5020a..62a0ec8 100644 --- a/src/weapon_range_audio.nvgt +++ b/src/weapon_range_audio.nvgt @@ -3,32 +3,39 @@ funcdef void WeaponRangeAudioCallback(); -WeaponRangeAudioCallback@[] weapon_range_audio_callbacks; +WeaponRangeAudioCallback @[] weapon_range_audio_callbacks; bool weapon_range_audio_initialized = false; void init_weapon_range_audio_registry() { - if (weapon_range_audio_initialized) return; + if (weapon_range_audio_initialized) + return; weapon_range_audio_callbacks.resize(0); weapon_range_audio_initialized = true; } -bool register_weapon_range_audio_callback(WeaponRangeAudioCallback@ callback) { +bool register_weapon_range_audio_callback(WeaponRangeAudioCallback @callback) { init_weapon_range_audio_registry(); - if (@callback is null) return false; + if (@callback is null) + return false; weapon_range_audio_callbacks.insert_last(callback); return true; } int get_current_weapon_range() { - if (sling_equipped) return SLING_RANGE; - if (bow_equipped) return BOW_RANGE; - if (spear_equipped) return 1; - if (axe_equipped) return 0; + if (sling_equipped) + return SLING_RANGE; + if (bow_equipped) + return BOW_RANGE; + if (spear_equipped) + return 1; + if (axe_equipped) + return 0; return -1; } void play_weapon_range_sound(string sound_file, int creature_pos) { - if (!file_exists(sound_file)) return; + if (!audio_asset_exists(sound_file)) + return; // Notification sound: keep centered on the player like footsteps int handle = p.play_stationary(sound_file, false); if (handle != -1) { @@ -36,7 +43,7 @@ void play_weapon_range_sound(string sound_file, int creature_pos) { } } -void update_weapon_range_audio(int creature_pos, bool &inout was_in_range) { +void update_weapon_range_audio(int creature_pos, bool& inout was_in_range) { int range = get_current_weapon_range(); bool in_range = (range >= 0) && (abs(creature_pos - x) <= range); if (in_range && !was_in_range) { @@ -47,15 +54,16 @@ void update_weapon_range_audio(int creature_pos, bool &inout was_in_range) { was_in_range = in_range; } -void force_weapon_range_exit(int creature_pos, bool &inout was_in_range) { - if (!was_in_range) return; +void force_weapon_range_exit(int creature_pos, bool& inout was_in_range) { + if (!was_in_range) + return; play_weapon_range_sound("sounds/enemies/exit_range.ogg", creature_pos); was_in_range = false; } void update_weapon_range_audio_all() { for (uint i = 0; i < weapon_range_audio_callbacks.length(); i++) { - WeaponRangeAudioCallback@ callback = weapon_range_audio_callbacks[i]; + WeaponRangeAudioCallback @callback = weapon_range_audio_callbacks[i]; if (@callback !is null) { callback(); } diff --git a/src/weather.nvgt b/src/weather.nvgt index 2017c48..17679d1 100644 --- a/src/weather.nvgt +++ b/src/weather.nvgt @@ -21,12 +21,12 @@ bool rain_fading = false; float rain_fade_from_volume = WEATHER_MIN_VOLUME; float rain_fade_to_volume = WEATHER_MIN_VOLUME; timer rain_fade_timer; -int rain_fade_duration = 0; // Custom duration per fade +int rain_fade_duration = 0; // Custom duration per fade // Thunder object state class ThunderStrike { int position; - int direction; // -1 = moving west, 1 = moving east, 0 = stationary + int direction; // -1 = moving west, 1 = moving east, 0 = stationary int sound_handle; timer movement_timer; int movement_speed; @@ -47,12 +47,13 @@ class ThunderStrike { } void update() { - if (direction == 0) return; // Stationary thunder + if (direction == 0) + return; // Stationary thunder if (movement_timer.elapsed >= movement_speed) { position += direction; movement_timer.restart(); - + // Update sound position if (sound_handle != -1 && p.sound_is_active(sound_handle)) { p.update_sound_1d(sound_handle, position); @@ -65,23 +66,16 @@ class ThunderStrike { } } -ThunderStrike@[] active_thunder; +ThunderStrike @[] active_thunder; timer thunder_timer; int next_thunder_interval = 0; // Sound file paths -string[] wind_sounds = { - "", // INTENSITY_NONE placeholder - "sounds/nature/wind_low.ogg", - "sounds/nature/wind_medium.ogg", - "sounds/nature/wind_high.ogg" -}; +string[] wind_sounds = {"", // INTENSITY_NONE placeholder + "sounds/nature/wind_low.ogg", "sounds/nature/wind_medium.ogg", "sounds/nature/wind_high.ogg"}; -string[] thunder_sounds = { - "sounds/nature/thunder_low.ogg", - "sounds/nature/thunder_medium.ogg", - "sounds/nature/thunder_high.ogg" -}; +string[] thunder_sounds = {"sounds/nature/thunder_low.ogg", "sounds/nature/thunder_medium.ogg", + "sounds/nature/thunder_high.ogg"}; void init_weather() { stop_all_weather_sounds(); @@ -236,8 +230,10 @@ void maybe_change_wind_intensity() { if (random(1, 100) <= 30) { int change = random(-1, 1); int new_intensity = wind_intensity + change; - if (new_intensity < INTENSITY_LOW) new_intensity = INTENSITY_LOW; - if (new_intensity > INTENSITY_HIGH) new_intensity = INTENSITY_HIGH; + if (new_intensity < INTENSITY_LOW) + new_intensity = INTENSITY_LOW; + if (new_intensity > INTENSITY_HIGH) + new_intensity = INTENSITY_HIGH; wind_intensity = new_intensity; } } @@ -246,8 +242,10 @@ void maybe_change_rain_intensity() { if (random(1, 100) <= 30) { int change = random(-1, 1); int new_intensity = rain_intensity + change; - if (new_intensity < INTENSITY_LOW) new_intensity = INTENSITY_LOW; - if (new_intensity > INTENSITY_HIGH) new_intensity = INTENSITY_HIGH; + if (new_intensity < INTENSITY_LOW) + new_intensity = INTENSITY_LOW; + if (new_intensity > INTENSITY_HIGH) + new_intensity = INTENSITY_HIGH; if (new_intensity != rain_intensity) { fade_rain_to_intensity(new_intensity); } @@ -258,15 +256,19 @@ void maybe_change_storm_intensity() { // Possibly change wind if (random(1, 100) <= 25) { int new_wind = wind_intensity + random(-1, 1); - if (new_wind < INTENSITY_LOW) new_wind = INTENSITY_LOW; - if (new_wind > INTENSITY_HIGH) new_wind = INTENSITY_HIGH; + if (new_wind < INTENSITY_LOW) + new_wind = INTENSITY_LOW; + if (new_wind > INTENSITY_HIGH) + new_wind = INTENSITY_HIGH; wind_intensity = new_wind; } // Possibly change rain if (random(1, 100) <= 25) { int new_rain = rain_intensity + random(-1, 1); - if (new_rain < INTENSITY_LOW) new_rain = INTENSITY_LOW; - if (new_rain > INTENSITY_HIGH) new_rain = INTENSITY_HIGH; + if (new_rain < INTENSITY_LOW) + new_rain = INTENSITY_LOW; + if (new_rain > INTENSITY_HIGH) + new_rain = INTENSITY_HIGH; if (new_rain != rain_intensity) { fade_rain_to_intensity(new_rain); } @@ -275,7 +277,8 @@ void maybe_change_storm_intensity() { // Wind gust implementation void update_wind_gusts() { - if (wind_intensity == INTENSITY_NONE) return; + if (wind_intensity == INTENSITY_NONE) + return; // Check if it's time for next gust if (wind_gust_timer.elapsed >= next_wind_gust_delay) { @@ -286,7 +289,8 @@ void update_wind_gusts() { } void play_wind_gust() { - if (wind_intensity == INTENSITY_NONE || wind_intensity > INTENSITY_HIGH) return; + if (wind_intensity == INTENSITY_NONE || wind_intensity > INTENSITY_HIGH) + return; // Clean up previous wind gust if it's stale if (wind_sound_handle != -1 && !p.sound_is_active(wind_sound_handle)) { @@ -300,14 +304,18 @@ void play_wind_gust() { // Rain fade implementation float get_rain_target_volume(int intensity) { - if (intensity == INTENSITY_NONE) return WEATHER_MIN_VOLUME; - if (intensity == INTENSITY_LOW) return RAIN_VOLUME_LIGHT; - if (intensity == INTENSITY_MEDIUM) return RAIN_VOLUME_MODERATE; + if (intensity == INTENSITY_NONE) + return WEATHER_MIN_VOLUME; + if (intensity == INTENSITY_LOW) + return RAIN_VOLUME_LIGHT; + if (intensity == INTENSITY_MEDIUM) + return RAIN_VOLUME_MODERATE; return RAIN_VOLUME_HEAVY; } void fade_rain_to_intensity(int new_intensity) { - if (new_intensity == rain_intensity && !rain_fading) return; + if (new_intensity == rain_intensity && !rain_fading) + return; // Complete any current fade if (rain_fading) { @@ -337,18 +345,20 @@ void fade_rain_to_intensity(int new_intensity) { rain_fading = true; rain_fade_timer.restart(); - rain_intensity = new_intensity; // Track target intensity + rain_intensity = new_intensity; // Track target intensity } void update_rain_fade() { - if (!rain_fading) return; + if (!rain_fading) + return; if (rain_sound_handle == -1) { rain_fading = false; return; } float progress = float(rain_fade_timer.elapsed) / float(rain_fade_duration); - if (progress > 1.0) progress = 1.0; + if (progress > 1.0) + progress = 1.0; float current_vol = rain_fade_from_volume + ((rain_fade_to_volume - rain_fade_from_volume) * progress); p.update_sound_start_values(rain_sound_handle, 0.0, current_vol, 1.0); @@ -384,7 +394,8 @@ void update_thunder() { } } - if (!thunder_enabled) return; + if (!thunder_enabled) + return; // Check if it's time for new thunder if (thunder_timer.elapsed >= next_thunder_interval) { @@ -407,7 +418,7 @@ void spawn_thunder() { if (random(1, 100) > 20) { // Thunder moves in random direction - may move toward or away from player int spawn_direction = random(0, 1) == 0 ? -1 : 1; - direction = spawn_direction; // Movement direction matches spawn side + direction = spawn_direction; // Movement direction matches spawn side int thunder_pos = x + (distance * spawn_direction); // Random speed: 100ms (fast rip) to 1200ms (slow roll) - halved from original @@ -417,7 +428,7 @@ void spawn_thunder() { int handle = play_1d_with_volume_step(thunder_file, x, thunder_pos, false, THUNDER_SOUND_VOLUME_STEP); if (handle != -1) { - ThunderStrike@ strike = ThunderStrike(thunder_pos, direction, handle, speed); + ThunderStrike @strike = ThunderStrike(thunder_pos, direction, handle, speed); active_thunder.insert_last(strike); } } else { @@ -429,7 +440,7 @@ void spawn_thunder() { int handle = play_1d_with_volume_step(thunder_file, x, thunder_pos, false, THUNDER_SOUND_VOLUME_STEP); if (handle != -1) { - ThunderStrike@ strike = ThunderStrike(thunder_pos, 0, handle, 0); + ThunderStrike @strike = ThunderStrike(thunder_pos, 0, handle, 0); active_thunder.insert_last(strike); } } @@ -462,7 +473,7 @@ string serialize_weather() { } void deserialize_weather(string data) { - string[]@ parts = data.split("|"); + string[] @parts = data.split("|"); if (parts.length() < 4) { init_weather(); return; diff --git a/src/world/mountains.nvgt b/src/world/mountains.nvgt index 4477e0a..fcad558 100644 --- a/src/world/mountains.nvgt +++ b/src/world/mountains.nvgt @@ -32,8 +32,10 @@ class MountainRange { // Clamp values to valid range for (int i = 0; i < size; i++) { - if (elevations[i] < MOUNTAIN_MIN_ELEVATION) elevations[i] = MOUNTAIN_MIN_ELEVATION; - if (elevations[i] > MOUNTAIN_MAX_ELEVATION) elevations[i] = MOUNTAIN_MAX_ELEVATION; + if (elevations[i] < MOUNTAIN_MIN_ELEVATION) + elevations[i] = MOUNTAIN_MIN_ELEVATION; + if (elevations[i] > MOUNTAIN_MAX_ELEVATION) + elevations[i] = MOUNTAIN_MAX_ELEVATION; } // Smooth to enforce max slope constraint @@ -63,7 +65,8 @@ class MountainRange { } void midpoint_displace(int left, int right, int roughness) { - if (right - left <= 1) return; + if (right - left <= 1) + return; int mid = (left + right) / 2; int avg = (elevations[left] + elevations[right]) / 2; @@ -71,7 +74,8 @@ class MountainRange { elevations[mid] = avg + displacement; int new_roughness = roughness * 7 / 10; - if (new_roughness < 1) new_roughness = 1; + if (new_roughness < 1) + new_roughness = 1; midpoint_displace(left, mid, new_roughness); midpoint_displace(mid, right, new_roughness); @@ -85,12 +89,12 @@ class MountainRange { iterations++; for (int i = 1; i < int(elevations.length()); i++) { - int diff = elevations[i] - elevations[i-1]; + int diff = elevations[i] - elevations[i - 1]; if (diff > MOUNTAIN_MAX_SLOPE) { - elevations[i] = elevations[i-1] + MOUNTAIN_MAX_SLOPE; + elevations[i] = elevations[i - 1] + MOUNTAIN_MAX_SLOPE; changed = true; } else if (diff < -MOUNTAIN_MAX_SLOPE) { - elevations[i] = elevations[i-1] - MOUNTAIN_MAX_SLOPE; + elevations[i] = elevations[i - 1] - MOUNTAIN_MAX_SLOPE; changed = true; } } @@ -105,7 +109,7 @@ class MountainRange { // Find all steep sections (positions where rope would be needed) int[] steep_positions; for (int i = 1; i < size; i++) { - if (abs(elevations[i] - elevations[i-1]) >= MOUNTAIN_STEEP_THRESHOLD) { + if (abs(elevations[i] - elevations[i - 1]) >= MOUNTAIN_STEEP_THRESHOLD) { steep_positions.insert_last(i); } } @@ -115,7 +119,8 @@ class MountainRange { int steep_pos = steep_positions[s]; // Check if we have enough tiles after this steep section for a plateau - if (steep_pos + MIN_PLATEAU_TILES >= size) continue; + if (steep_pos + MIN_PLATEAU_TILES >= size) + continue; // Get the elevation after the steep climb int plateau_start_elevation = elevations[steep_pos]; @@ -123,7 +128,8 @@ class MountainRange { // Ensure next MIN_PLATEAU_TILES have gentle slopes for (int p = 1; p <= MIN_PLATEAU_TILES; p++) { int tile_idx = steep_pos + p; - if (tile_idx >= size) break; + if (tile_idx >= size) + break; // Calculate max allowed elevation change for gentle slope int prev_elevation = elevations[tile_idx - 1]; @@ -146,7 +152,8 @@ class MountainRange { // Final pass: ensure no negative elevations anywhere for (int i = 0; i < size; i++) { - if (elevations[i] < 0) elevations[i] = 0; + if (elevations[i] < 0) + elevations[i] = 0; } // Prefer climbing up before descending by adjusting early terrain @@ -170,15 +177,19 @@ class MountainRange { // Find valley bottoms (local minima) int[] valleys; for (int i = 2; i < int(elevations.length()) - 2; i++) { - if (elevations[i] < elevations[i-1] && elevations[i] < elevations[i+1] && - elevations[i] < elevations[i-2] && elevations[i] < elevations[i+2]) { - valleys.insert_last(i); + if (elevations[i] < elevations[i - 1] && elevations[i] < elevations[i + 1] && + elevations[i] < elevations[i - 2] && elevations[i] < elevations[i + 2]) { + // Keep mountain streams off both extreme edge tiles. + if (i > 0 && i < int(elevations.length()) - 1) { + valleys.insert_last(i); + } } } // Place 1-3 streams in valleys int num_streams = random(1, 3); - if (num_streams > int(valleys.length())) num_streams = int(valleys.length()); + if (num_streams > int(valleys.length())) + num_streams = int(valleys.length()); for (int i = 0; i < num_streams && valleys.length() > 0; i++) { int idx = random(0, int(valleys.length()) - 1); @@ -193,12 +204,12 @@ class MountainRange { // Check if we already have a steep climb (≥7) to/from 10-20 elevation bool has_steep_climb = false; for (int i = 1; i < size; i++) { - int diff = abs(elevations[i] - elevations[i-1]); + int diff = abs(elevations[i] - elevations[i - 1]); if (diff >= MOUNTAIN_STEEP_THRESHOLD) { // Check if either elevation is in 10-20 range if ((elevations[i] >= 10 && elevations[i] <= 20) || - (elevations[i-1] >= 10 && elevations[i-1] <= 20)) { + (elevations[i - 1] >= 10 && elevations[i - 1] <= 20)) { has_steep_climb = true; break; } @@ -248,24 +259,26 @@ class MountainRange { // Last transition: tile size-2 to tile size-1 if (size >= 2) { - if (abs(elevations[size-1] - elevations[size-2]) > 20) { - if (elevations[size-2] > elevations[size-1]) { - elevations[size-2] = elevations[size-1] + 20; + if (abs(elevations[size - 1] - elevations[size - 2]) > 20) { + if (elevations[size - 2] > elevations[size - 1]) { + elevations[size - 2] = elevations[size - 1] + 20; } else { - elevations[size-2] = elevations[size-1] - 20; + elevations[size - 2] = elevations[size - 1] - 20; } } } } int get_elevation_at(int world_x) { - if (world_x < start_position || world_x > end_position) return 0; + if (world_x < start_position || world_x > end_position) + return 0; int index = world_x - start_position; return elevations[index]; } string get_terrain_at(int world_x) { - if (world_x < start_position || world_x > end_position) return "stone"; + if (world_x < start_position || world_x > end_position) + return "stone"; int index = world_x - start_position; return terrain_types[index]; } @@ -285,16 +298,19 @@ class MountainRange { } bool is_stream_at(int world_x) { - if (!contains_position(world_x)) return false; + if (!contains_position(world_x)) + return false; int index = world_x - start_position; for (uint i = 0; i < stream_positions.length(); i++) { - if (stream_positions[i] == index) return true; + if (stream_positions[i] == index) + return true; } return false; } void update() { - if (stream_positions.length() == 0) return; + if (stream_positions.length() == 0) + return; // Find nearest stream to player int nearest_stream = -1; @@ -336,10 +352,9 @@ class MountainRange { } stream_sound_position = -1; } -} -MountainRange@[] world_mountains; +} MountainRange @[] world_mountains; -MountainRange@ get_mountain_at(int world_x) { +MountainRange @get_mountain_at(int world_x) { for (uint i = 0; i < world_mountains.length(); i++) { if (world_mountains[i].contains_position(world_x)) { return @world_mountains[i]; @@ -349,21 +364,24 @@ MountainRange@ get_mountain_at(int world_x) { } int get_mountain_elevation_at(int world_x) { - MountainRange@ mountain = get_mountain_at(world_x); - if (mountain is null) return 0; + MountainRange @mountain = get_mountain_at(world_x); + if (mountain is null) + return 0; return mountain.get_elevation_at(world_x); } bool is_mountain_stream_at(int world_x) { - MountainRange@ mountain = get_mountain_at(world_x); - if (mountain is null) return false; + MountainRange @mountain = get_mountain_at(world_x); + if (mountain is null) + return false; return mountain.is_stream_at(world_x); } -void update_mountains_with_audio_areas(int[]@ areaStarts, int[]@ areaEnds) { +void update_mountains_with_audio_areas(int[] @areaStarts, int[] @areaEnds) { bool limit_audio = (areaStarts.length() > 0); for (uint i = 0; i < world_mountains.length(); i++) { - if (limit_audio && !range_overlaps_active_areas(world_mountains[i].start_position, world_mountains[i].end_position, areaStarts, areaEnds)) { + if (limit_audio && !range_overlaps_active_areas(world_mountains[i].start_position, + world_mountains[i].end_position, areaStarts, areaEnds)) { world_mountains[i].destroy(); continue; } diff --git a/src/world/world_buildings.nvgt b/src/world/world_buildings.nvgt index d12ea0e..b7d47a8 100644 --- a/src/world/world_buildings.nvgt +++ b/src/world/world_buildings.nvgt @@ -7,8 +7,7 @@ class WorldFirepit { WorldFirepit(int pos) { position = pos; } -} -WorldFirepit@[] world_firepits; +} WorldFirepit @[] world_firepits; class WorldHerbGarden { int position; @@ -16,8 +15,7 @@ class WorldHerbGarden { WorldHerbGarden(int pos) { position = pos; } -} -WorldHerbGarden@[] world_herb_gardens; +} WorldHerbGarden @[] world_herb_gardens; class WorldStorage { int position; @@ -25,8 +23,7 @@ class WorldStorage { WorldStorage(int pos) { position = pos; } -} -WorldStorage@[] world_storages; +} WorldStorage @[] world_storages; class WorldPasture { int position; @@ -34,8 +31,7 @@ class WorldPasture { WorldPasture(int pos) { position = pos; } -} -WorldPasture@[] world_pastures; +} WorldPasture @[] world_pastures; class WorldStable { int position; @@ -43,8 +39,7 @@ class WorldStable { WorldStable(int pos) { position = pos; } -} -WorldStable@[] world_stables; +} WorldStable @[] world_stables; class WorldAltar { int position; @@ -52,8 +47,7 @@ class WorldAltar { WorldAltar(int pos) { position = pos; } -} -WorldAltar@[] world_altars; +} WorldAltar @[] world_altars; int horses_count = 0; int livestock_count = 0; @@ -61,17 +55,17 @@ int storage_level = STORAGE_LEVEL_BASE; // Add functions void add_world_firepit(int pos) { - WorldFirepit@ fp = WorldFirepit(pos); + WorldFirepit @fp = WorldFirepit(pos); world_firepits.insert_last(fp); } void add_world_herb_garden(int pos) { - WorldHerbGarden@ hg = WorldHerbGarden(pos); + WorldHerbGarden @hg = WorldHerbGarden(pos); world_herb_gardens.insert_last(hg); } void add_world_storage(int pos) { - WorldStorage@ s = WorldStorage(pos); + WorldStorage @s = WorldStorage(pos); world_storages.insert_last(s); } @@ -82,22 +76,22 @@ void ensure_base_storage() { } void add_world_pasture(int pos) { - WorldPasture@ p = WorldPasture(pos); + WorldPasture @p = WorldPasture(pos); world_pastures.insert_last(p); } void add_world_stable(int pos) { - WorldStable@ s = WorldStable(pos); + WorldStable @s = WorldStable(pos); world_stables.insert_last(s); } void add_world_altar(int pos) { - WorldAltar@ a = WorldAltar(pos); + WorldAltar @a = WorldAltar(pos); world_altars.insert_last(a); } // Getter functions -WorldFirepit@ get_firepit_at(int pos) { +WorldFirepit @get_firepit_at(int pos) { for (uint i = 0; i < world_firepits.length(); i++) { if (world_firepits[i].position == pos) { return @world_firepits[i]; @@ -106,10 +100,10 @@ WorldFirepit@ get_firepit_at(int pos) { return null; } -WorldFirepit@ get_firepit_near(int pos, int range) { +WorldFirepit @get_firepit_near(int pos, int range) { // Check for firepit within specified range for (int check_x = pos - range; check_x <= pos + range; check_x++) { - WorldFirepit@ firepit = get_firepit_at(check_x); + WorldFirepit @firepit = get_firepit_at(check_x); if (firepit != null) { return @firepit; } @@ -117,7 +111,7 @@ WorldFirepit@ get_firepit_near(int pos, int range) { return null; } -WorldHerbGarden@ get_herb_garden_at(int pos) { +WorldHerbGarden @get_herb_garden_at(int pos) { for (uint i = 0; i < world_herb_gardens.length(); i++) { if (world_herb_gardens[i].position == pos) { return @world_herb_gardens[i]; @@ -126,7 +120,7 @@ WorldHerbGarden@ get_herb_garden_at(int pos) { return null; } -WorldHerbGarden@ get_herb_garden_at_base() { +WorldHerbGarden @get_herb_garden_at_base() { // Check if herb garden exists anywhere in base area (0-4) for (uint i = 0; i < world_herb_gardens.length(); i++) { if (world_herb_gardens[i].position <= BASE_END) { @@ -136,7 +130,7 @@ WorldHerbGarden@ get_herb_garden_at_base() { return null; } -WorldStorage@ get_storage_at(int pos) { +WorldStorage @get_storage_at(int pos) { for (uint i = 0; i < world_storages.length(); i++) { if (world_storages[i].position == pos) { return @world_storages[i]; @@ -145,7 +139,7 @@ WorldStorage@ get_storage_at(int pos) { return null; } -WorldPasture@ get_pasture_at(int pos) { +WorldPasture @get_pasture_at(int pos) { for (uint i = 0; i < world_pastures.length(); i++) { if (world_pastures[i].position == pos) { return @world_pastures[i]; @@ -154,7 +148,7 @@ WorldPasture@ get_pasture_at(int pos) { return null; } -WorldStable@ get_stable_at(int pos) { +WorldStable @get_stable_at(int pos) { for (uint i = 0; i < world_stables.length(); i++) { if (world_stables[i].position == pos) { return @world_stables[i]; @@ -163,7 +157,7 @@ WorldStable@ get_stable_at(int pos) { return null; } -WorldAltar@ get_altar_at(int pos) { +WorldAltar @get_altar_at(int pos) { for (uint i = 0; i < world_altars.length(); i++) { if (world_altars[i].position == pos) { return @world_altars[i]; diff --git a/src/world/world_drops.nvgt b/src/world/world_drops.nvgt index 0ddb206..c6be598 100644 --- a/src/world/world_drops.nvgt +++ b/src/world/world_drops.nvgt @@ -13,24 +13,24 @@ class WorldDrop { // Start looping item sound at position sound_handle = p.play_1d("sounds/items/item.ogg", x, position, true); if (sound_handle != -1) { - p.update_sound_positioning_values(sound_handle, -1.0, 3.0, true); + p.update_sound_positioning_values(sound_handle, -1.0, 3.0, true); } } void update() { if (sound_handle == -1 || !p.sound_is_active(sound_handle)) { - // Clean up invalid handle if it exists - if (sound_handle != -1) { - p.destroy_sound(sound_handle); - sound_handle = -1; - } - sound_handle = p.play_1d("sounds/items/item.ogg", x, position, true); - if (sound_handle != -1) { - p.update_sound_positioning_values(sound_handle, -1.0, 3.0, true); - } + // Clean up invalid handle if it exists + if (sound_handle != -1) { + p.destroy_sound(sound_handle); + sound_handle = -1; + } + sound_handle = p.play_1d("sounds/items/item.ogg", x, position, true); + if (sound_handle != -1) { + p.update_sound_positioning_values(sound_handle, -1.0, 3.0, true); + } } else { - // Update source position for 1d sound - p.update_sound_1d(sound_handle, position); + // Update source position for 1d sound + p.update_sound_1d(sound_handle, position); } } @@ -40,11 +40,10 @@ class WorldDrop { sound_handle = -1; } } -} -WorldDrop@[] world_drops; +} WorldDrop @[] world_drops; void add_world_drop(int pos, string type) { - WorldDrop@ d = WorldDrop(pos, type); + WorldDrop @d = WorldDrop(pos, type); world_drops.insert_last(d); } @@ -55,7 +54,8 @@ void update_world_drops() { bool limit_audio = (areaStarts.length() > 0); for (uint i = 0; i < world_drops.length(); i++) { - if (limit_audio && !range_overlaps_active_areas(world_drops[i].position, world_drops[i].position, areaStarts, areaEnds)) { + if (limit_audio && + !range_overlaps_active_areas(world_drops[i].position, world_drops[i].position, areaStarts, areaEnds)) { world_drops[i].destroy(); continue; } @@ -63,7 +63,7 @@ void update_world_drops() { } } -WorldDrop@ get_drop_at(int pos) { +WorldDrop @get_drop_at(int pos) { for (uint i = 0; i < world_drops.length(); i++) { if (world_drops[i].position == pos) { return @world_drops[i]; @@ -91,42 +91,50 @@ 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; } -bool try_pickup_world_drop(WorldDrop@ drop) { +bool try_pickup_world_drop(WorldDrop @drop) { if (get_flying_creature_config_by_drop_type(drop.type) !is null) { return try_pickup_small_game(drop.type); } 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/src/world/world_fires.nvgt b/src/world/world_fires.nvgt index 25de87d..e29f39a 100644 --- a/src/world/world_fires.nvgt +++ b/src/world/world_fires.nvgt @@ -35,13 +35,19 @@ class WorldFire { // Warn when fuel is low (30 seconds remaining) if (!low_fuel_warned && fuel_remaining <= 30000 && fuel_remaining > 0) { low_fuel_warned = true; - notify("Fire at x " + position + " y " + y + " is getting low!"); + dictionary fireLowArgs; + fireLowArgs.set("position", position); + fireLowArgs.set("y", y); + notify(trf("system.fire.low_fuel_at", fireLowArgs)); } // Fire went out if (fuel_remaining <= 0) { fuel_remaining = 0; - notify("Fire at x " + position + " y " + y + " has gone out."); + dictionary fireOutArgs; + fireOutArgs.set("position", position); + fireOutArgs.set("y", y); + notify(trf("system.fire.went_out_at", fireOutArgs)); if (sound_handle != -1) { p.destroy_sound(sound_handle); sound_handle = -1; @@ -78,11 +84,10 @@ class WorldFire { sound_handle = -1; } } -} -WorldFire@[] world_fires; +} WorldFire @[] world_fires; void add_world_fire(int pos) { - WorldFire@ f = WorldFire(pos); + WorldFire @f = WorldFire(pos); world_fires.insert_last(f); } @@ -95,7 +100,8 @@ void update_fires() { // Update all fires and remove any that have burned out for (uint i = 0; i < world_fires.length(); i++) { world_fires[i].update(); - if (limit_audio && !range_overlaps_active_areas(world_fires[i].position, world_fires[i].position, areaStarts, areaEnds)) { + if (limit_audio && + !range_overlaps_active_areas(world_fires[i].position, world_fires[i].position, areaStarts, areaEnds)) { if (world_fires[i].sound_handle != -1) { p.destroy_sound(world_fires[i].sound_handle); world_fires[i].sound_handle = -1; @@ -113,7 +119,7 @@ void update_fires() { } } -WorldFire@ get_fire_at(int pos) { +WorldFire @get_fire_at(int pos) { for (uint i = 0; i < world_fires.length(); i++) { if (world_fires[i].position == pos) { return @world_fires[i]; @@ -122,10 +128,10 @@ WorldFire@ get_fire_at(int pos) { return null; } -WorldFire@ get_fire_near(int pos) { +WorldFire @get_fire_near(int pos) { // Check for fire at current position or adjacent tiles for (int check_x = pos - 1; check_x <= pos + 1; check_x++) { - WorldFire@ fire = get_fire_at(check_x); + WorldFire @fire = get_fire_at(check_x); if (fire != null && fire.is_burning()) { return @fire; } @@ -133,10 +139,10 @@ WorldFire@ get_fire_near(int pos) { return null; } -WorldFire@ get_fire_within_range(int pos, int range) { +WorldFire @get_fire_within_range(int pos, int range) { // Check for fire within specified range for (int check_x = pos - range; check_x <= pos + range; check_x++) { - WorldFire@ fire = get_fire_at(check_x); + WorldFire @fire = get_fire_at(check_x); if (fire != null && fire.is_burning()) { return @fire; } diff --git a/src/world/world_snares.nvgt b/src/world/world_snares.nvgt index 3959111..3af0961 100644 --- a/src/world/world_snares.nvgt +++ b/src/world/world_snares.nvgt @@ -17,7 +17,7 @@ class WorldSnare { int escape_chance; int sound_handle; timer hour_timer; - bool active; // To prevent immediate breakage on placement + bool active; // To prevent immediate breakage on placement int hours_with_catch; // Track how long animal has been caught (game hours) WorldSnare(int pos) { @@ -51,7 +51,8 @@ class WorldSnare { } sound_handle = p.play_1d("sounds/actions/set_snare.ogg", x, position, true); if (sound_handle != -1) { - p.update_sound_positioning_values(sound_handle, SNARE_SOUND_PAN_STEP, SNARE_SOUND_VOLUME_STEP, true); + p.update_sound_positioning_values(sound_handle, SNARE_SOUND_PAN_STEP, SNARE_SOUND_VOLUME_STEP, + true); } } } else if (sound_handle != -1) { @@ -71,19 +72,24 @@ class WorldSnare { // After that: escape chance increases by 2% per game hour if (hours_with_catch > 1) { escape_chance = (hours_with_catch - 1) * 2; - if (escape_chance > 95) escape_chance = 95; + if (escape_chance > 95) + escape_chance = 95; int roll = random(1, 100); if (roll <= escape_chance) { // Animal escaped! - notify("A " + catch_type + " escaped from your snare at x " + position + " y 0!"); + dictionary snareEscapeArgs; + snareEscapeArgs.set("catch_type", i18n_translate_fragment_value(catch_type)); + snareEscapeArgs.set("position", position); + notify(trf("system.snare.escape_at", snareEscapeArgs)); remove_snare_at(position); return; } } } else { // Trying to catch small game - if (catch_chance < 75) catch_chance += 5; + if (catch_chance < 75) + catch_chance += 5; int roll = random(1, 100); if (roll <= catch_chance) { @@ -92,7 +98,10 @@ class WorldSnare { catch_type = get_random_small_game(); escape_chance = 0; hours_with_catch = 0; - notify(catch_type + " caught in snare at x " + position + " y 0!"); + dictionary snareCaughtArgs; + snareCaughtArgs.set("catch_type", i18n_translate_fragment_value(catch_type)); + snareCaughtArgs.set("position", position); + notify(trf("system.snare.caught_at", snareCaughtArgs)); } } } @@ -104,15 +113,14 @@ class WorldSnare { sound_handle = -1; } } -} -WorldSnare@[] world_snares; +} WorldSnare @[] world_snares; void add_world_snare(int pos) { - WorldSnare@ s = WorldSnare(pos); + WorldSnare @s = WorldSnare(pos); world_snares.insert_last(s); } -WorldSnare@ get_snare_at(int pos) { +WorldSnare @get_snare_at(int pos) { for (uint i = 0; i < world_snares.length(); i++) { if (world_snares[i].position == pos) { return @world_snares[i]; @@ -133,15 +141,17 @@ void remove_snare_at(int pos) { // Called when player moves onto a tile void check_snare_collision(int player_x) { - WorldSnare@ s = get_snare_at(player_x); + WorldSnare @s = get_snare_at(player_x); if (s != null && s.active) { // Break the snare p.play_stationary("sounds/actions/break_snare.ogg", false); if (s.has_catch) { - speak_with_history("You stepped on your snare! The " + s.catch_type + " escaped.", true); + dictionary snareSteppedArgs; + snareSteppedArgs.set("catch_type", i18n_translate_fragment_value(s.catch_type)); + speak_with_history(trf("system.snare.stepped_on_with_escape", snareSteppedArgs), true); } else { - speak_with_history("You stepped on your snare and broke it!", true); + speak_with_history(tr("system.snare.stepped_on_broke"), true); } remove_snare_at(player_x); @@ -155,7 +165,7 @@ void update_snares() { bool limit_audio = (areaStarts.length() > 0); for (int i = int(world_snares.length()) - 1; i >= 0; i--) { - WorldSnare@ snare = world_snares[i]; + WorldSnare @snare = world_snares[i]; snare.update(); if (limit_audio && !range_overlaps_active_areas(snare.position, snare.position, areaStarts, areaEnds)) { if (snare.sound_handle != -1) { diff --git a/src/world/world_streams.nvgt b/src/world/world_streams.nvgt index 4548157..0974269 100644 --- a/src/world/world_streams.nvgt +++ b/src/world/world_streams.nvgt @@ -63,8 +63,7 @@ class WorldStream { } sound_position = -1; } -} -WorldStream@[] world_streams; +} WorldStream @[] world_streams; string get_expanded_area_type(int index) { string terrain = expanded_terrain_types[index]; @@ -74,7 +73,7 @@ string get_expanded_area_type(int index) { return terrain; } -bool get_audio_area_bounds_for_position(int pos, int &out areaStart, int &out areaEnd) { +bool get_audio_area_bounds_for_position(int pos, int& out areaStart, int& out areaEnd) { if (pos <= BASE_END) { areaStart = 0; areaEnd = BASE_END; @@ -97,18 +96,21 @@ bool get_audio_area_bounds_for_position(int pos, int &out areaStart, int &out ar int index = pos - expanded_area_start; int total = int(expanded_terrain_types.length()); - if (index < 0 || index >= total) return false; + if (index < 0 || index >= total) + return false; string areaType = get_expanded_area_type(index); int left = index; while (left > 0) { - if (get_expanded_area_type(left - 1) != areaType) break; + if (get_expanded_area_type(left - 1) != areaType) + break; left--; } int right = index; while (right + 1 < total) { - if (get_expanded_area_type(right + 1) != areaType) break; + if (get_expanded_area_type(right + 1) != areaType) + break; right++; } @@ -117,13 +119,14 @@ bool get_audio_area_bounds_for_position(int pos, int &out areaStart, int &out ar return true; } -void get_active_audio_areas(int[]@ areaStarts, int[]@ areaEnds) { +void get_active_audio_areas(int[] @areaStarts, int[] @areaEnds) { areaStarts.resize(0); areaEnds.resize(0); int currentStart = 0; int currentEnd = 0; - if (!get_audio_area_bounds_for_position(x, currentStart, currentEnd)) return; + if (!get_audio_area_bounds_for_position(x, currentStart, currentEnd)) + return; areaStarts.insert_last(currentStart); areaEnds.insert_last(currentEnd); @@ -151,7 +154,7 @@ void get_active_audio_areas(int[]@ areaStarts, int[]@ areaEnds) { } } -bool range_overlaps_active_areas(int rangeStart, int rangeEnd, int[]@ areaStarts, int[]@ areaEnds) { +bool range_overlaps_active_areas(int rangeStart, int rangeEnd, int[] @areaStarts, int[] @areaEnds) { for (uint i = 0; i < areaStarts.length(); i++) { if (rangeEnd >= areaStarts[i] && rangeStart <= areaEnds[i]) { return true; @@ -161,7 +164,7 @@ bool range_overlaps_active_areas(int rangeStart, int rangeEnd, int[]@ areaStarts } void add_world_stream(int start_pos, int width) { - WorldStream@ s = WorldStream(start_pos, width); + WorldStream @s = WorldStream(start_pos, width); world_streams.insert_last(s); } @@ -172,7 +175,8 @@ void update_streams() { bool limit_audio = areaStarts.length() > 0; for (uint i = 0; i < world_streams.length(); i++) { - if (limit_audio && !range_overlaps_active_areas(world_streams[i].start_position, world_streams[i].end_position, areaStarts, areaEnds)) { + if (limit_audio && !range_overlaps_active_areas(world_streams[i].start_position, world_streams[i].end_position, + areaStarts, areaEnds)) { world_streams[i].destroy(); continue; } @@ -182,7 +186,7 @@ void update_streams() { update_mountains_with_audio_areas(areaStarts, areaEnds); } -WorldStream@ get_stream_at(int pos) { +WorldStream @get_stream_at(int pos) { for (uint i = 0; i < world_streams.length(); i++) { if (world_streams[i].contains_position(pos)) { return @world_streams[i]; @@ -196,7 +200,8 @@ bool is_position_in_water(int pos) { } bool is_deep_stream_at(int pos) { - WorldStream@ stream = get_stream_at(pos); - if (stream == null) return false; + WorldStream @stream = get_stream_at(pos); + if (stream == null) + return false; return stream.get_width() > 3; } 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.