From dfd6a0f3a1af986c02c59b2c534a8bc2ca9ae816 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Sat, 24 Jan 2026 00:57:00 -0500 Subject: [PATCH] A few bug fixes, new item, canoe, added. --- AGENTS.md | 142 +++++++++++++++++++++++++++++----- README.md | 2 +- draugnorak.nvgt | 14 ++-- sounds/terrain/deep_water.ogg | 3 + src/audio_utils.nvgt | 3 + src/constants.nvgt | 2 +- src/crafting/craft_tools.nvgt | 86 +++++++++++++++++++- src/enemies/undead.nvgt | 9 ++- src/environment.nvgt | 113 +++++++++++++++++++++++---- src/item_registry.nvgt | 5 +- src/menus/action_menu.nvgt | 1 + src/time_system.nvgt | 4 +- src/world/world_streams.nvgt | 6 ++ 13 files changed, 339 insertions(+), 51 deletions(-) create mode 100644 sounds/terrain/deep_water.ogg diff --git a/AGENTS.md b/AGENTS.md index 912f958..3bc5fd5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,21 +8,49 @@ - `src/` - Game source code modules - `constants.nvgt` - Game configuration and constants - - `player.nvgt` - Player state and variables - - `world_state.nvgt` - World objects (snares, fires, firepits, herb gardens) - - `inventory.nvgt` - Inventory, crafting, equipment menus - - `environment.nvgt` - Trees and environmental interactions - - `combat.nvgt` - Combat system - - `time_system.nvgt` - In-game time tracking + - `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 - - `notify.nvgt` - Notification system + - `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 -- All item stacks are capped at 9 until backpacks are implemented +- 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) @@ -41,21 +69,23 @@ - New fires start with 12 minutes (720000ms) of fuel - Warning at 30 seconds remaining - Fire goes out when fuel depletes -- Fire sound plays within 2 tiles distance +- 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 minute for catching/escaping rabbits +- Check every minute for catching/escaping small game - Catch chance starts at 5%, increases by 5% per minute (max 75%) - Escape chance starts at 5%, increases by 5% per minute (max 95%) - Snare sound plays within 2 tiles distance ### Tree System -- Trees regenerate after 4 minutes (240000ms) when depleted or chopped -- Tree ambient sound plays within 3 tiles distance +- 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) @@ -65,6 +95,7 @@ - `]` - 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 @@ -75,7 +106,7 @@ - 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 (5-tile range) +- 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 @@ -84,25 +115,98 @@ - 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 three categories: -- **Weapons**: Spear -- **Tools**: Stone Knife, Snare, Stone Axe -- **Buildings**: Firepit, Fire, Herb Garden +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 use `./nvgt -c draugnorak.nvgt` to compile without opening the game window -- This prevents the window from taking over the terminal during 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 +- See `docs/patterns.md` for module and creature patterns +- See `docs/rune_system.md` for rune data/effects/save requirements diff --git a/README.md b/README.md index 5a2465c..fe153b7 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ Crafting is only available in the base (C). ### Categories - **Weapons**: Spear, Sling -- **Tools**: Knife, Snare, Stone Axe, Fishing Pole, Rope, Quiver, Reed Basket, Clay Pot +- **Tools**: Knife, Snare, Stone Axe, Fishing Pole, Rope, Quiver, Canoe, Reed Basket, Clay Pot - **Materials**: Arrows, Butcher Game, Incense - **Clothing**: Skin gear, Moccasins, Pouch, Backpack - **Buildings**: Firepit, Fire, Herb Garden, Storage, Pasture, Stable, Altar diff --git a/draugnorak.nvgt b/draugnorak.nvgt index 048136c..d03af85 100644 --- a/draugnorak.nvgt +++ b/draugnorak.nvgt @@ -332,9 +332,10 @@ void main() if(key_down(KEY_LEFT) && x > 0 && !climbing && !falling && !rope_climbing) { facing = 0; - // Check mountain movement - if (can_move_mountain(x, x - 1)) { - x--; + int target_x = x - 1; + // Check mountain movement and deep water + if (can_move_mountain(x, target_x) && can_enter_stream_tile(target_x)) { + x = target_x; // Always update elevation to match ground (0 if not in mountain) int new_elevation = get_mountain_elevation_at(x); if (new_elevation != y) { @@ -350,9 +351,10 @@ void main() else if(key_down(KEY_RIGHT) && x < MAP_SIZE - 1 && !climbing && !falling && !rope_climbing) { facing = 1; - // Check mountain movement - if (can_move_mountain(x, x + 1)) { - x++; + int target_x = x + 1; + // Check mountain movement and deep water + if (can_move_mountain(x, target_x) && can_enter_stream_tile(target_x)) { + x = target_x; // Always update elevation to match ground (0 if not in mountain) int new_elevation = get_mountain_elevation_at(x); if (new_elevation != y) { diff --git a/sounds/terrain/deep_water.ogg b/sounds/terrain/deep_water.ogg new file mode 100644 index 0000000..9d64342 --- /dev/null +++ b/sounds/terrain/deep_water.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c25c426403aa373da22159934f697c1b438098c35d0a147b08b986068cb38a6 +size 15653 diff --git a/src/audio_utils.nvgt b/src/audio_utils.nvgt index 0469df2..82d62e3 100644 --- a/src/audio_utils.nvgt +++ b/src/audio_utils.nvgt @@ -1,6 +1,9 @@ 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"; + } if (is_position_in_water(current_x) || is_mountain_stream_at(current_x)) { return "sounds/terrain/shallow_water.ogg"; } diff --git a/src/constants.nvgt b/src/constants.nvgt index 111866e..019cb70 100644 --- a/src/constants.nvgt +++ b/src/constants.nvgt @@ -43,6 +43,7 @@ const int ARROW_CAPACITY_PER_QUIVER = 12; // Zombie settings const int ZOMBIE_HEALTH = 12; const int ZOMBIE_MAX_COUNT = 5; +const int ZOMBIE_MAX_COUNT_CAP = 12; const int ZOMBIE_MOVE_INTERVAL = 1000; const int ZOMBIE_ATTACK_INTERVAL = 1600; const int ZOMBIE_DAMAGE_MIN = 4; @@ -239,4 +240,3 @@ const int RESIDENT_COLLECTION_CHANCE = 10; // 10% chance per basket per hour int abs(int value) { return value < 0 ? -value : value; } - diff --git a/src/crafting/craft_tools.nvgt b/src/crafting/craft_tools.nvgt index ed0d169..9cebe4f 100644 --- a/src/crafting/craft_tools.nvgt +++ b/src/crafting/craft_tools.nvgt @@ -10,6 +10,7 @@ void run_tools_menu() { "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)" }; @@ -41,8 +42,9 @@ void run_tools_menu() { else if (selection == 3) craft_fishing_pole(); else if (selection == 4) craft_rope(); else if (selection == 5) craft_quiver(); - else if (selection == 6) craft_reed_basket(); - else if (selection == 7) craft_clay_pot(); + else if (selection == 6) craft_canoe(); + else if (selection == 7) craft_reed_basket(); + else if (selection == 8) craft_clay_pot(); break; } @@ -53,8 +55,9 @@ void run_tools_menu() { 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_reed_basket_max(); - else if (selection == 7) craft_clay_pot_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; } } @@ -296,6 +299,81 @@ void craft_quiver_max() { speak_with_history("Crafted " + maxCraft + " Quivers.", 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 (missing == "") { + if (get_personal_count(ITEM_CANOES) >= get_personal_stack_limit()) { + speak_with_history("You can't carry any more canoes.", true); + return; + } + simulate_crafting(40); + add_personal_count(ITEM_LOGS, -4); + add_personal_count(ITEM_STICKS, -11); + add_personal_count(ITEM_VINES, -11); + add_personal_count(ITEM_SKINS, -6); + 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); + } else { + speak_with_history("Missing: " + missing, true); + } +} + +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); + return; + } + + int maxByLogs = get_personal_count(ITEM_LOGS) / 4; + int maxBySticks = get_personal_count(ITEM_STICKS) / 11; + int maxByVines = get_personal_count(ITEM_VINES) / 11; + int maxBySkins = get_personal_count(ITEM_SKINS) / 6; + 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; + + int space = get_personal_stack_limit() - get_personal_count(ITEM_CANOES); + 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); + return; + } + + int totalCost = maxCraft * 40; + int craftTime = (totalCost < maxCraft * 4) ? maxCraft * 4 : totalCost; + simulate_crafting(craftTime); + add_personal_count(ITEM_LOGS, -(maxCraft * 4)); + add_personal_count(ITEM_STICKS, -(maxCraft * 11)); + add_personal_count(ITEM_VINES, -(maxCraft * 11)); + add_personal_count(ITEM_SKINS, -(maxCraft * 6)); + 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); +} + void craft_reed_basket() { string missing = ""; if (get_personal_count(ITEM_REEDS) < 3) missing += "3 reeds "; diff --git a/src/enemies/undead.nvgt b/src/enemies/undead.nvgt index 790fbca..07db618 100644 --- a/src/enemies/undead.nvgt +++ b/src/enemies/undead.nvgt @@ -183,7 +183,14 @@ void update_undeads() { return; } - while (undeads.length() < ZOMBIE_MAX_COUNT) { + int extra = 0; + if (MAP_SIZE > 35) { + extra = (MAP_SIZE - 35) / 15; + } + int maxCount = ZOMBIE_MAX_COUNT + extra; + if (maxCount > ZOMBIE_MAX_COUNT_CAP) maxCount = ZOMBIE_MAX_COUNT_CAP; + + while (undeads.length() < maxCount) { spawn_undead(); } diff --git a/src/environment.nvgt b/src/environment.nvgt index 5a55333..f09c9da 100644 --- a/src/environment.nvgt +++ b/src/environment.nvgt @@ -65,7 +65,8 @@ class Tree { int areaStart = 0; int areaEnd = 0; - if (!get_tree_area_bounds_for_position(position, areaStart, areaEnd)) { + string areaTerrain = ""; + if (!get_tree_area_bounds_for_position(position, areaStart, areaEnd, areaTerrain)) { areaStart = BASE_END + 1; areaEnd = GRASS_END; } @@ -182,10 +183,38 @@ class Tree { } Tree@[] trees; -bool get_tree_area_bounds_for_position(int pos, int &out areaStart, int &out areaEnd) { +string get_tree_area_terrain(int areaStart, int areaEnd) { + if (areaStart >= BASE_END + 1 && areaEnd <= GRASS_END) { + return "grass"; + } + + if (expanded_area_start != -1 && areaStart >= expanded_area_start && areaEnd <= expanded_area_end) { + int index = areaStart - expanded_area_start; + if (index >= 0 && index < int(expanded_terrain_types.length())) { + string terrain = expanded_terrain_types[index]; + if (terrain.find("mountain:") == 0) { + terrain = terrain.substr(9); + } + return terrain; + } + } + + return "grass"; +} + +int get_tree_max_for_area(int areaStart, int areaEnd) { + string terrain = get_tree_area_terrain(areaStart, areaEnd); + if (terrain == "forest" || terrain == "deep_forest") { + return TREE_MAX_PER_AREA + 1; + } + return TREE_MAX_PER_AREA; +} + +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; + areaTerrain = "grass"; return true; } @@ -195,21 +224,36 @@ 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 (expanded_terrain_types[index] != "grass") 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; int left = index; - while (left > 0 && expanded_terrain_types[left - 1] == "grass") { + while (left > 0) { + string leftTerrain = expanded_terrain_types[left - 1]; + if (leftTerrain.find("mountain:") == 0) { + leftTerrain = leftTerrain.substr(9); + } + if (leftTerrain != terrain) break; left--; } int right = index; int maxIndex = int(expanded_terrain_types.length()) - 1; - while (right < maxIndex && expanded_terrain_types[right + 1] == "grass") { + while (right < maxIndex) { + string rightTerrain = expanded_terrain_types[right + 1]; + if (rightTerrain.find("mountain:") == 0) { + rightTerrain = rightTerrain.substr(9); + } + if (rightTerrain != terrain) break; right++; } areaStart = expanded_area_start + left; areaEnd = expanded_area_start + right; + areaTerrain = terrain; return true; } @@ -242,7 +286,8 @@ bool tree_too_close_in_area(int pos, int areaStart, int areaEnd, Tree@ ignoreTre } bool place_tree_in_area(Tree@ tree, int areaStart, int areaEnd) { - if (count_trees_in_area(areaStart, areaEnd, tree) >= TREE_MAX_PER_AREA) { + int maxTrees = get_tree_max_for_area(areaStart, areaEnd); + if (count_trees_in_area(areaStart, areaEnd, tree) >= maxTrees) { return false; } @@ -259,7 +304,8 @@ bool place_tree_in_area(Tree@ tree, int areaStart, int areaEnd) { } bool spawn_tree_in_area(int areaStart, int areaEnd) { - if (count_trees_in_area(areaStart, areaEnd, null) >= TREE_MAX_PER_AREA) { + int maxTrees = get_tree_max_for_area(areaStart, areaEnd); + if (count_trees_in_area(areaStart, areaEnd, null) >= maxTrees) { return false; } @@ -280,7 +326,7 @@ void spawn_trees(int grass_start, int grass_end) { spawn_tree_in_area(grass_start, grass_end); } -void get_grass_areas(int[]@ areaStarts, int[]@ areaEnds) { +void get_tree_areas(int[]@ areaStarts, int[]@ areaEnds) { areaStarts.resize(0); areaEnds.resize(0); @@ -291,9 +337,18 @@ void get_grass_areas(int[]@ areaStarts, int[]@ areaEnds) { int total = int(expanded_terrain_types.length()); int index = 0; while (index < total) { - if (expanded_terrain_types[index] == "grass") { + string terrain = expanded_terrain_types[index]; + if (terrain.find("mountain:") == 0) { + terrain = terrain.substr(9); + } + if (terrain == "grass" || terrain == "forest" || terrain == "deep_forest") { int segmentStart = index; - while (index + 1 < total && expanded_terrain_types[index + 1] == "grass") { + while (index + 1 < total) { + string nextTerrain = expanded_terrain_types[index + 1]; + if (nextTerrain.find("mountain:") == 0) { + nextTerrain = nextTerrain.substr(9); + } + if (nextTerrain != terrain) break; index++; } int segmentEnd = index; @@ -306,7 +361,8 @@ void get_grass_areas(int[]@ areaStarts, int[]@ areaEnds) { bool relocate_tree_to_any_area(Tree@ tree, int[]@ areaStarts, int[]@ areaEnds) { for (uint i = 0; i < areaStarts.length(); i++) { - if (count_trees_in_area(areaStarts[i], areaEnds[i], tree) >= TREE_MAX_PER_AREA) continue; + int maxTrees = get_tree_max_for_area(areaStarts[i], areaEnds[i]); + if (count_trees_in_area(areaStarts[i], areaEnds[i], tree) >= maxTrees) continue; if (place_tree_in_area(tree, areaStarts[i], areaEnds[i])) { return true; } @@ -317,13 +373,14 @@ bool relocate_tree_to_any_area(Tree@ tree, int[]@ areaStarts, int[]@ areaEnds) { void normalize_tree_positions() { int[] areaStarts; int[] areaEnds; - get_grass_areas(areaStarts, areaEnds); + get_tree_areas(areaStarts, areaEnds); if (areaStarts.length() == 0) return; for (uint i = 0; i < trees.length(); i++) { int areaStart = 0; int areaEnd = 0; - if (!get_tree_area_bounds_for_position(trees[i].position, areaStart, areaEnd)) { + string areaTerrain = ""; + if (!get_tree_area_bounds_for_position(trees[i].position, areaStart, areaEnd, areaTerrain)) { if (!relocate_tree_to_any_area(trees[i], areaStarts, areaEnds)) { if (trees[i].sound_handle != -1) { p.destroy_sound(trees[i].sound_handle); @@ -337,6 +394,7 @@ void normalize_tree_positions() { for (uint areaIndex = 0; areaIndex < areaStarts.length(); areaIndex++) { int areaStart = areaStarts[areaIndex]; int areaEnd = areaEnds[areaIndex]; + int maxTrees = get_tree_max_for_area(areaStart, areaEnd); int[] areaTreeIndices; for (uint i = 0; i < trees.length(); i++) { @@ -345,7 +403,7 @@ void normalize_tree_positions() { } } - while (areaTreeIndices.length() > TREE_MAX_PER_AREA) { + while (areaTreeIndices.length() > maxTrees) { uint treeIndex = areaTreeIndices[areaTreeIndices.length() - 1]; Tree@ tree = trees[treeIndex]; if (!relocate_tree_to_any_area(tree, areaStarts, areaEnds)) { @@ -880,16 +938,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_with_history("You'll need a rope to climb there.", true); + speak_movement_blocked("You'll need a rope to climb there."); return false; } // Prompt for rope climb int elevation_change = mountain.get_elevation_change(from_x, to_x); if (elevation_change > 0) { - speak_with_history("Press up to climb up.", true); + speak_movement_blocked("Press up to climb up."); } else { - speak_with_history("Press down to climb down.", true); + speak_movement_blocked("Press down to climb down."); } // Store pending rope climb info @@ -901,6 +959,27 @@ bool can_move_mountain(int from_x, int to_x) { return true; } +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."); + return false; +} + +timer movementBlockTimer; +string lastMovementBlockMessage = ""; +const int MOVEMENT_BLOCK_COOLDOWN_MS = 3000; + +void speak_movement_blocked(string message) { + if (message == lastMovementBlockMessage && movementBlockTimer.elapsed < MOVEMENT_BLOCK_COOLDOWN_MS) { + return; + } + + lastMovementBlockMessage = message; + movementBlockTimer.restart(); + speak_with_history(message, true); +} + // Rope climbing functions void start_rope_climb(bool climbing_up, int target_x, int target_elevation) { rope_climbing = true; diff --git a/src/item_registry.nvgt b/src/item_registry.nvgt index 9b5e20f..b0bd159 100644 --- a/src/item_registry.nvgt +++ b/src/item_registry.nvgt @@ -36,7 +36,8 @@ const int ITEM_BOWSTRINGS = 30; const int ITEM_SINEW = 31; const int ITEM_BOAR_CARCASSES = 32; const int ITEM_BACKPACKS = 33; -const int ITEM_COUNT = 34; // Total number of item types +const int ITEM_CANOES = 34; +const int ITEM_COUNT = 35; // Total number of item types // Item definition class class ItemDefinition { @@ -116,6 +117,7 @@ void init_item_registry() { 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_BACKPACKS] = ItemDefinition(ITEM_BACKPACKS, "backpacks", "backpack", "Backpacks", 2.50); + item_registry[ITEM_CANOES] = ItemDefinition(ITEM_CANOES, "canoes", "canoe", "Canoes", 4.00); // Define display order for inventory menus // This controls the order items appear in menus @@ -152,6 +154,7 @@ void init_item_registry() { ITEM_ROPES, ITEM_REED_BASKETS, ITEM_CLAY_POTS, + ITEM_CANOES, // Clothing ITEM_SKIN_HATS, ITEM_SKIN_GLOVES, diff --git a/src/menus/action_menu.nvgt b/src/menus/action_menu.nvgt index ddaa912..a9a3dcb 100644 --- a/src/menus/action_menu.nvgt +++ b/src/menus/action_menu.nvgt @@ -71,6 +71,7 @@ void try_burn_incense() { } add_personal_count(ITEM_INCENSE, -1); + 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); diff --git a/src/time_system.nvgt b/src/time_system.nvgt index f9fd4ad..3d27021 100644 --- a/src/time_system.nvgt +++ b/src/time_system.nvgt @@ -393,7 +393,6 @@ void update_time() { if (current_hour == 6) { process_daily_weapon_breakage(); attempt_daily_quest(); - save_game_state(); } attempt_daily_invasion(); keep_base_fires_fed(); @@ -404,6 +403,9 @@ void update_time() { attempt_blessing(); check_weather_transition(); attempt_resident_collection(); + if (current_hour == 6) { + save_game_state(); + } } ensure_ambience_running(); diff --git a/src/world/world_streams.nvgt b/src/world/world_streams.nvgt index 9e7af01..34f0234 100644 --- a/src/world/world_streams.nvgt +++ b/src/world/world_streams.nvgt @@ -89,3 +89,9 @@ WorldStream@ get_stream_at(int pos) { bool is_position_in_water(int pos) { return get_stream_at(pos) != null; } + +bool is_deep_stream_at(int pos) { + WorldStream@ stream = get_stream_at(pos); + if (stream == null) return false; + return stream.get_width() > 3; +}