A few bug fixes, new item, canoe, added.

This commit is contained in:
Storm Dragon
2026-01-24 00:57:00 -05:00
parent 78e8d434a3
commit dfd6a0f3a1
13 changed files with 339 additions and 51 deletions

142
AGENTS.md
View File

@@ -8,21 +8,49 @@
- `src/` - Game source code modules - `src/` - Game source code modules
- `constants.nvgt` - Game configuration and constants - `constants.nvgt` - Game configuration and constants
- `player.nvgt` - Player state and variables - `player.nvgt` - Player state and global timers
- `world_state.nvgt` - World objects (snares, fires, firepits, herb gardens) - `world_state.nvgt` - World object orchestration (legacy coordinator, modules are in `src/world/` and `src/enemies/`)
- `inventory.nvgt` - Inventory, crafting, equipment menus - `item_registry.nvgt` - Item definitions and inventory arrays
- `environment.nvgt` - Trees and environmental interactions - `inventory_items.nvgt` - Equipment, quick slots, stack limits
- `combat.nvgt` - Combat system - `inventory_menus.nvgt` - Menu orchestrator for inventory/storage/equipment/actions
- `time_system.nvgt` - In-game time tracking - `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 - `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 - `sounds/` - Game audio assets
- `draugnorak.nvgt` - Main game file - `draugnorak.nvgt` - Main game file
## Game Mechanics ## Game Mechanics
### Inventory Limits ### 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 ### Map Layout
- Base area: x 0-4 (wood terrain) - Base area: x 0-4 (wood terrain)
@@ -41,21 +69,23 @@
- New fires start with 12 minutes (720000ms) of fuel - New fires start with 12 minutes (720000ms) of fuel
- Warning at 30 seconds remaining - Warning at 30 seconds remaining
- Fire goes out when fuel depletes - 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 - Jumping over fire prevents damage
- Feeding fire is done from the Action menu (A)
### Snare System ### Snare System
- Snares become active when player moves away - 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%) - Catch chance starts at 5%, increases by 5% per minute (max 75%)
- Escape chance starts at 5%, increases by 5% per minute (max 95%) - Escape chance starts at 5%, increases by 5% per minute (max 95%)
- Snare sound plays within 2 tiles distance - Snare sound plays within 2 tiles distance
### Tree System ### Tree System
- Trees regenerate after 4 minutes (240000ms) when depleted or chopped - Trees regenerate fully after ~5 minutes (minute-by-minute refill logic)
- Tree ambient sound plays within 3 tiles distance - Tree ambient sound plays within 4 tiles distance
- Trees only play sound when not chopped - 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) - 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 ### Notification System
- Important events use `notify()` function (plays sounds/notify.ogg + speaks message) - Important events use `notify()` function (plays sounds/notify.ogg + speaks message)
@@ -65,6 +95,7 @@
- `]` - Next (newer) notification - `]` - Next (newer) notification
- `[` (Shift+Comma) - Previous (older) notification - `[` (Shift+Comma) - Previous (older) notification
- Navigating history speaks message without notification sound - Navigating history speaks message without notification sound
- Speech history (screen_reader_speak wrapper) uses `,` and `.` for previous/next message
### Search System ### Search System
- Hold Shift for 1 second to search - Hold Shift for 1 second to search
@@ -75,7 +106,7 @@
- Zombies cannot enter the base while barricade health > 0 - 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 attack the barricade for 4-6 damage when they reach it; play `sounds/enemies/zombie_hit.ogg`
- Zombies vanish at daybreak - 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 ### Barricade System
- Base starts with 100 barricade health, capped at 500 - 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) - Reinforcement costs and health: 3 sticks (+10), 5 vines (+15), 1 log (+30), 5 stones (+20)
- Barricade health does not reset during gameplay - 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 ## Menu Structure
### Crafting Menu (C key, base only) ### Crafting Menu (C key, base only)
Organized into three categories: Organized into categories:
- **Weapons**: Spear - **Weapons**: Spear, Sling
- **Tools**: Stone Knife, Snare, Stone Axe - **Tools**: Knife, Snare, Stone Axe, Fishing Pole, Rope, Reed Basket, Clay Pot
- **Buildings**: Firepit, Fire, Herb Garden - **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) ### Equipment Menu (E key)
- Only shows items player actually has - Only shows items player actually has
- Shows equipped status - Shows equipped status
- Says "Nothing to equip" if inventory is empty - 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 ## Testing
- Always use `./nvgt -c draugnorak.nvgt` to compile without opening the game window - Always run `./nvgt -c draugnorak.nvgt` after code changes
- This prevents the window from taking over the terminal during testing - This compiles without opening the game window and prevents it from taking over the terminal
## Code Standards ## Code Standards
- Use `notify()` for important game events that should be reviewable - Use `notify()` for important game events that should be reviewable
- Use `screen_reader_speak()` for immediate feedback that doesn't need to be stored - 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

View File

@@ -57,7 +57,7 @@ Crafting is only available in the base (C).
### Categories ### Categories
- **Weapons**: Spear, Sling - **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 - **Materials**: Arrows, Butcher Game, Incense
- **Clothing**: Skin gear, Moccasins, Pouch, Backpack - **Clothing**: Skin gear, Moccasins, Pouch, Backpack
- **Buildings**: Firepit, Fire, Herb Garden, Storage, Pasture, Stable, Altar - **Buildings**: Firepit, Fire, Herb Garden, Storage, Pasture, Stable, Altar

View File

@@ -332,9 +332,10 @@ void main()
if(key_down(KEY_LEFT) && x > 0 && !climbing && !falling && !rope_climbing) if(key_down(KEY_LEFT) && x > 0 && !climbing && !falling && !rope_climbing)
{ {
facing = 0; facing = 0;
// Check mountain movement int target_x = x - 1;
if (can_move_mountain(x, x - 1)) { // Check mountain movement and deep water
x--; 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) // Always update elevation to match ground (0 if not in mountain)
int new_elevation = get_mountain_elevation_at(x); int new_elevation = get_mountain_elevation_at(x);
if (new_elevation != y) { if (new_elevation != y) {
@@ -350,9 +351,10 @@ void main()
else if(key_down(KEY_RIGHT) && x < MAP_SIZE - 1 && !climbing && !falling && !rope_climbing) else if(key_down(KEY_RIGHT) && x < MAP_SIZE - 1 && !climbing && !falling && !rope_climbing)
{ {
facing = 1; facing = 1;
// Check mountain movement int target_x = x + 1;
if (can_move_mountain(x, x + 1)) { // Check mountain movement and deep water
x++; 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) // Always update elevation to match ground (0 if not in mountain)
int new_elevation = get_mountain_elevation_at(x); int new_elevation = get_mountain_elevation_at(x);
if (new_elevation != y) { if (new_elevation != y) {

BIN
sounds/terrain/deep_water.ogg LFS Normal file

Binary file not shown.

View File

@@ -1,6 +1,9 @@
string get_footstep_sound(int current_x, int base_end, int grass_end) string get_footstep_sound(int current_x, int base_end, int grass_end)
{ {
// Check if in water first (regular streams or mountain streams) // 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)) { if (is_position_in_water(current_x) || is_mountain_stream_at(current_x)) {
return "sounds/terrain/shallow_water.ogg"; return "sounds/terrain/shallow_water.ogg";
} }

View File

@@ -43,6 +43,7 @@ const int ARROW_CAPACITY_PER_QUIVER = 12;
// Zombie settings // Zombie settings
const int ZOMBIE_HEALTH = 12; const int ZOMBIE_HEALTH = 12;
const int ZOMBIE_MAX_COUNT = 5; const int ZOMBIE_MAX_COUNT = 5;
const int ZOMBIE_MAX_COUNT_CAP = 12;
const int ZOMBIE_MOVE_INTERVAL = 1000; const int ZOMBIE_MOVE_INTERVAL = 1000;
const int ZOMBIE_ATTACK_INTERVAL = 1600; const int ZOMBIE_ATTACK_INTERVAL = 1600;
const int ZOMBIE_DAMAGE_MIN = 4; 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) { int abs(int value) {
return value < 0 ? -value : value; return value < 0 ? -value : value;
} }

View File

@@ -10,6 +10,7 @@ void run_tools_menu() {
"Fishing Pole (1 Stick, 2 Vines)", "Fishing Pole (1 Stick, 2 Vines)",
"Rope (3 Vines)", "Rope (3 Vines)",
"Quiver (2 Skins, 2 Vines)", "Quiver (2 Skins, 2 Vines)",
"Canoe (4 Logs, 11 Sticks, 11 Vines, 6 Skins, 2 Rope, 6 Reeds)",
"Reed Basket (3 Reeds)", "Reed Basket (3 Reeds)",
"Clay Pot (3 Clay)" "Clay Pot (3 Clay)"
}; };
@@ -41,8 +42,9 @@ void run_tools_menu() {
else if (selection == 3) craft_fishing_pole(); else if (selection == 3) craft_fishing_pole();
else if (selection == 4) craft_rope(); else if (selection == 4) craft_rope();
else if (selection == 5) craft_quiver(); else if (selection == 5) craft_quiver();
else if (selection == 6) craft_reed_basket(); else if (selection == 6) craft_canoe();
else if (selection == 7) craft_clay_pot(); else if (selection == 7) craft_reed_basket();
else if (selection == 8) craft_clay_pot();
break; break;
} }
@@ -53,8 +55,9 @@ void run_tools_menu() {
else if (selection == 3) craft_fishing_pole_max(); else if (selection == 3) craft_fishing_pole_max();
else if (selection == 4) craft_rope_max(); else if (selection == 4) craft_rope_max();
else if (selection == 5) craft_quiver_max(); else if (selection == 5) craft_quiver_max();
else if (selection == 6) craft_reed_basket_max(); else if (selection == 6) craft_canoe_max();
else if (selection == 7) craft_clay_pot_max(); else if (selection == 7) craft_reed_basket_max();
else if (selection == 8) craft_clay_pot_max();
break; break;
} }
} }
@@ -296,6 +299,81 @@ void craft_quiver_max() {
speak_with_history("Crafted " + maxCraft + " Quivers.", true); 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() { void craft_reed_basket() {
string missing = ""; string missing = "";
if (get_personal_count(ITEM_REEDS) < 3) missing += "3 reeds "; if (get_personal_count(ITEM_REEDS) < 3) missing += "3 reeds ";

View File

@@ -183,7 +183,14 @@ void update_undeads() {
return; 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(); spawn_undead();
} }

View File

@@ -65,7 +65,8 @@ class Tree {
int areaStart = 0; int areaStart = 0;
int areaEnd = 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; areaStart = BASE_END + 1;
areaEnd = GRASS_END; areaEnd = GRASS_END;
} }
@@ -182,10 +183,38 @@ class Tree {
} }
Tree@[] trees; 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) { if (pos >= BASE_END + 1 && pos <= GRASS_END) {
areaStart = BASE_END + 1; areaStart = BASE_END + 1;
areaEnd = GRASS_END; areaEnd = GRASS_END;
areaTerrain = "grass";
return true; 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; 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;
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; 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--; left--;
} }
int right = index; int right = index;
int maxIndex = int(expanded_terrain_types.length()) - 1; 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++; right++;
} }
areaStart = expanded_area_start + left; areaStart = expanded_area_start + left;
areaEnd = expanded_area_start + right; areaEnd = expanded_area_start + right;
areaTerrain = terrain;
return true; 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) { 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; 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) { 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; return false;
} }
@@ -280,7 +326,7 @@ void spawn_trees(int grass_start, int grass_end) {
spawn_tree_in_area(grass_start, 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); areaStarts.resize(0);
areaEnds.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 total = int(expanded_terrain_types.length());
int index = 0; int index = 0;
while (index < total) { 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; 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++; index++;
} }
int segmentEnd = 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) { bool relocate_tree_to_any_area(Tree@ tree, int[]@ areaStarts, int[]@ areaEnds) {
for (uint i = 0; i < areaStarts.length(); i++) { 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])) { if (place_tree_in_area(tree, areaStarts[i], areaEnds[i])) {
return true; return true;
} }
@@ -317,13 +373,14 @@ bool relocate_tree_to_any_area(Tree@ tree, int[]@ areaStarts, int[]@ areaEnds) {
void normalize_tree_positions() { void normalize_tree_positions() {
int[] areaStarts; int[] areaStarts;
int[] areaEnds; int[] areaEnds;
get_grass_areas(areaStarts, areaEnds); get_tree_areas(areaStarts, areaEnds);
if (areaStarts.length() == 0) return; if (areaStarts.length() == 0) return;
for (uint i = 0; i < trees.length(); i++) { for (uint i = 0; i < trees.length(); i++) {
int areaStart = 0; int areaStart = 0;
int areaEnd = 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 (!relocate_tree_to_any_area(trees[i], areaStarts, areaEnds)) {
if (trees[i].sound_handle != -1) { if (trees[i].sound_handle != -1) {
p.destroy_sound(trees[i].sound_handle); p.destroy_sound(trees[i].sound_handle);
@@ -337,6 +394,7 @@ void normalize_tree_positions() {
for (uint areaIndex = 0; areaIndex < areaStarts.length(); areaIndex++) { for (uint areaIndex = 0; areaIndex < areaStarts.length(); areaIndex++) {
int areaStart = areaStarts[areaIndex]; int areaStart = areaStarts[areaIndex];
int areaEnd = areaEnds[areaIndex]; int areaEnd = areaEnds[areaIndex];
int maxTrees = get_tree_max_for_area(areaStart, areaEnd);
int[] areaTreeIndices; int[] areaTreeIndices;
for (uint i = 0; i < trees.length(); i++) { 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]; uint treeIndex = areaTreeIndices[areaTreeIndices.length() - 1];
Tree@ tree = trees[treeIndex]; Tree@ tree = trees[treeIndex];
if (!relocate_tree_to_any_area(tree, areaStarts, areaEnds)) { 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)) { if (mountain.is_steep_section(from_x, to_x)) {
// Need rope // Need rope
if (get_personal_count(ITEM_ROPES) < 1) { 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; return false;
} }
// Prompt for rope climb // Prompt for rope climb
int elevation_change = mountain.get_elevation_change(from_x, to_x); int elevation_change = mountain.get_elevation_change(from_x, to_x);
if (elevation_change > 0) { if (elevation_change > 0) {
speak_with_history("Press up to climb up.", true); speak_movement_blocked("Press up to climb up.");
} else { } else {
speak_with_history("Press down to climb down.", true); speak_movement_blocked("Press down to climb down.");
} }
// Store pending rope climb info // Store pending rope climb info
@@ -901,6 +959,27 @@ bool can_move_mountain(int from_x, int to_x) {
return true; 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 // Rope climbing functions
void start_rope_climb(bool climbing_up, int target_x, int target_elevation) { void start_rope_climb(bool climbing_up, int target_x, int target_elevation) {
rope_climbing = true; rope_climbing = true;

View File

@@ -36,7 +36,8 @@ const int ITEM_BOWSTRINGS = 30;
const int ITEM_SINEW = 31; const int ITEM_SINEW = 31;
const int ITEM_BOAR_CARCASSES = 32; const int ITEM_BOAR_CARCASSES = 32;
const int ITEM_BACKPACKS = 33; 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 // Item definition class
class ItemDefinition { 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_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_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 // Define display order for inventory menus
// This controls the order items appear in menus // This controls the order items appear in menus
@@ -152,6 +154,7 @@ void init_item_registry() {
ITEM_ROPES, ITEM_ROPES,
ITEM_REED_BASKETS, ITEM_REED_BASKETS,
ITEM_CLAY_POTS, ITEM_CLAY_POTS,
ITEM_CANOES,
// Clothing // Clothing
ITEM_SKIN_HATS, ITEM_SKIN_HATS,
ITEM_SKIN_GLOVES, ITEM_SKIN_GLOVES,

View File

@@ -71,6 +71,7 @@ void try_burn_incense() {
} }
add_personal_count(ITEM_INCENSE, -1); add_personal_count(ITEM_INCENSE, -1);
add_personal_count(ITEM_CLAY_POTS, -1);
incense_hours_remaining += INCENSE_HOURS_PER_STICK; incense_hours_remaining += INCENSE_HOURS_PER_STICK;
incense_burning = true; incense_burning = true;
speak_with_history("Incense burning. " + incense_hours_remaining + " hours remaining.", true); speak_with_history("Incense burning. " + incense_hours_remaining + " hours remaining.", true);

View File

@@ -393,7 +393,6 @@ void update_time() {
if (current_hour == 6) { if (current_hour == 6) {
process_daily_weapon_breakage(); process_daily_weapon_breakage();
attempt_daily_quest(); attempt_daily_quest();
save_game_state();
} }
attempt_daily_invasion(); attempt_daily_invasion();
keep_base_fires_fed(); keep_base_fires_fed();
@@ -404,6 +403,9 @@ void update_time() {
attempt_blessing(); attempt_blessing();
check_weather_transition(); check_weather_transition();
attempt_resident_collection(); attempt_resident_collection();
if (current_hour == 6) {
save_game_state();
}
} }
ensure_ambience_running(); ensure_ambience_running();

View File

@@ -89,3 +89,9 @@ WorldStream@ get_stream_at(int pos) {
bool is_position_in_water(int pos) { bool is_position_in_water(int pos) {
return get_stream_at(pos) != null; 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;
}