A few bug fixes, new item, canoe, added.
This commit is contained in:
142
AGENTS.md
142
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
BIN
sounds/terrain/deep_water.ogg
LFS
Normal file
BIN
sounds/terrain/deep_water.ogg
LFS
Normal file
Binary file not shown.
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ";
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user