diff --git a/src/constants.nvgt b/src/constants.nvgt index 9115107..afcbae9 100644 --- a/src/constants.nvgt +++ b/src/constants.nvgt @@ -129,6 +129,8 @@ const float STREAM_SOUND_VOLUME_STEP = 4.3; // 30 dB over 7 tiles const float TREE_SOUND_VOLUME_STEP = 4.0; // Similar to snares for good audibility const int TREE_SOUND_RANGE = 4; +const int TREE_MIN_DISTANCE = 10; +const int TREE_MAX_PER_AREA = 2; const float RESIDENT_DEFENSE_VOLUME_STEP = 3.0; // Default volume for resident counter-attacks const float PLAYER_WEAPON_SOUND_VOLUME_STEP = 3.0; @@ -174,3 +176,29 @@ const int GOOSE_MAX_DIST_FROM_WATER = 4; // How far they can wander from water const int GOOSE_MAX_COUNT = 3; const int GOOSE_HOURLY_SPAWN_CHANCE = 35; // Percent chance per hour to spawn a goose const int GOOSE_SIGHT_RANGE = 0; + +// Weather settings +const int WEATHER_FADE_DURATION = 8000; // 8 seconds for smooth audio transitions +const float WEATHER_MIN_VOLUME = -30.0; +const float WEATHER_MAX_VOLUME = 0.0; +const float RAIN_VOLUME_LIGHT = -18.0; +const float RAIN_VOLUME_MODERATE = -10.0; +const float RAIN_VOLUME_HEAVY = -3.0; +const int WIND_GUST_MIN_DELAY = 30000; // Min 30 seconds between gusts +const int WIND_GUST_MAX_DELAY = 60000; // Max 60 seconds between gusts +const int THUNDER_MIN_INTERVAL = 8000; // Min 8 seconds between thunder +const int THUNDER_MAX_INTERVAL = 35000; // Max 35 seconds between thunder +const int THUNDER_MOVEMENT_SPEED = 2000; // ms per tile movement (slow roll across sky) +const float THUNDER_SOUND_VOLUME_STEP = 2.0; // Gentler volume falloff +const int THUNDER_SPAWN_DISTANCE_MIN = 20; // Min distance from player +const int THUNDER_SPAWN_DISTANCE_MAX = 40; // Max distance from player +const int CHANCE_CLEAR_TO_WINDY = 15; +const int CHANCE_CLEAR_TO_RAINY = 6; +const int CHANCE_CLEAR_TO_STORMY = 5; +const int CHANCE_WINDY_STAY = 55; +const int CHANCE_WINDY_TO_CLEAR = 25; +const int CHANCE_WINDY_TO_STORMY = 12; +const int CHANCE_RAINY_STAY = 40; +const int CHANCE_RAINY_TO_STORMY = 35; +const int CHANCE_STORMY_STAY = 40; +const int CHANCE_STORMY_TO_RAINY = 35; diff --git a/src/environment.nvgt b/src/environment.nvgt index 978d1d2..2e4c738 100644 --- a/src/environment.nvgt +++ b/src/environment.nvgt @@ -28,7 +28,6 @@ void apply_falling_damage(int fall_height) { // Feedback screen_reader_speak("Fell " + fall_height + " feet! Took " + damage + " damage. " + player_health + " health remaining.", true); } - // Tree Object class Tree { int position; @@ -61,12 +60,24 @@ class Tree { minutes_since_depletion = 0; } - void respawn(int grass_start, int grass_end) { + void respawn() { if (sound_handle != -1) { p.destroy_sound(sound_handle); sound_handle = -1; } - position = random(grass_start, grass_end); + + int areaStart = 0; + int areaEnd = 0; + if (!get_tree_area_bounds_for_position(position, areaStart, areaEnd)) { + areaStart = BASE_END + 1; + areaEnd = GRASS_END; + } + + Tree@ currentTree = @this; + if (!place_tree_in_area(currentTree, areaStart, areaEnd)) { + return; + } + refill(); } @@ -106,7 +117,7 @@ class Tree { if (is_chopped) { if (minutes_since_depletion >= 5) { - respawn(BASE_END + 1, GRASS_END); + respawn(); } return; } @@ -170,33 +181,200 @@ class Tree { } Tree@[] trees; -bool tree_too_close(int pos) { - // Check distance from base (must be at least 5 tiles away) - if (pos <= BASE_END + 5) { +bool get_tree_area_bounds_for_position(int pos, int &out areaStart, int &out areaEnd) { + if (pos >= BASE_END + 1 && pos <= GRASS_END) { + areaStart = BASE_END + 1; + areaEnd = GRASS_END; return true; } - // Check distance from other trees (must be at least 10 tiles apart) + if (expanded_area_start == -1 || pos < expanded_area_start || pos > expanded_area_end) { + return false; + } + + 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; + + int left = index; + while (left > 0 && expanded_terrain_types[left - 1] == "grass") { + left--; + } + + int right = index; + int maxIndex = int(expanded_terrain_types.length()) - 1; + while (right < maxIndex && expanded_terrain_types[right + 1] == "grass") { + right++; + } + + areaStart = expanded_area_start + left; + areaEnd = expanded_area_start + right; + return true; +} + +int count_trees_in_area(int areaStart, int areaEnd, Tree@ ignoreTree) { + int count = 0; for (uint i = 0; i < trees.length(); i++) { + if (@trees[i] is ignoreTree) continue; + if (trees[i].position >= areaStart && trees[i].position <= areaEnd) { + count++; + } + } + return count; +} + +bool tree_too_close_in_area(int pos, int areaStart, int areaEnd, Tree@ ignoreTree) { + // Keep trees away from the base edge + if (pos < BASE_END + 5) { + return true; + } + + for (uint i = 0; i < trees.length(); i++) { + if (@trees[i] is ignoreTree) continue; + if (trees[i].position < areaStart || trees[i].position > areaEnd) continue; + int distance = trees[i].position - pos; if (distance < 0) distance = -distance; - if (distance < 10) { + if (distance < TREE_MIN_DISTANCE) { return true; } } return false; } -void spawn_trees(int grass_start, int grass_end) { - int attempts = 10; +bool place_tree_in_area(Tree@ tree, int areaStart, int areaEnd) { + if (count_trees_in_area(areaStart, areaEnd, tree) >= TREE_MAX_PER_AREA) { + return false; + } + + int attempts = 20; for (int i = 0; i < attempts; i++) { - int pos = random(grass_start, grass_end); - if (tree_too_close(pos)) { + int pos = random(areaStart, areaEnd); + if (tree_too_close_in_area(pos, areaStart, areaEnd, tree)) { + continue; + } + tree.position = pos; + return true; + } + return false; +} + +bool spawn_tree_in_area(int areaStart, int areaEnd) { + if (count_trees_in_area(areaStart, areaEnd, null) >= TREE_MAX_PER_AREA) { + return false; + } + + int attempts = 20; + for (int i = 0; i < attempts; i++) { + int pos = random(areaStart, areaEnd); + if (tree_too_close_in_area(pos, areaStart, areaEnd, null)) { continue; } Tree@ t = Tree(pos); trees.insert_last(t); - return; + return true; + } + return false; +} + +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) { + areaStarts.resize(0); + areaEnds.resize(0); + + areaStarts.insert_last(BASE_END + 1); + areaEnds.insert_last(GRASS_END); + + if (expanded_area_start == -1) return; + int total = int(expanded_terrain_types.length()); + int index = 0; + while (index < total) { + if (expanded_terrain_types[index] == "grass") { + int segmentStart = index; + while (index + 1 < total && expanded_terrain_types[index + 1] == "grass") { + index++; + } + int segmentEnd = index; + areaStarts.insert_last(expanded_area_start + segmentStart); + areaEnds.insert_last(expanded_area_start + segmentEnd); + } + index++; + } +} + +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; + if (place_tree_in_area(tree, areaStarts[i], areaEnds[i])) { + return true; + } + } + return false; +} + +void normalize_tree_positions() { + int[] areaStarts; + int[] areaEnds; + get_grass_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)) { + if (!relocate_tree_to_any_area(trees[i], areaStarts, areaEnds)) { + if (trees[i].sound_handle != -1) { + p.destroy_sound(trees[i].sound_handle); + } + trees.remove_at(i); + i--; + } + } + } + + for (uint areaIndex = 0; areaIndex < areaStarts.length(); areaIndex++) { + int areaStart = areaStarts[areaIndex]; + int areaEnd = areaEnds[areaIndex]; + + int[] areaTreeIndices; + for (uint i = 0; i < trees.length(); i++) { + if (trees[i].position >= areaStart && trees[i].position <= areaEnd) { + areaTreeIndices.insert_last(i); + } + } + + while (areaTreeIndices.length() > TREE_MAX_PER_AREA) { + uint treeIndex = areaTreeIndices[areaTreeIndices.length() - 1]; + Tree@ tree = trees[treeIndex]; + if (!relocate_tree_to_any_area(tree, areaStarts, areaEnds)) { + if (tree.sound_handle != -1) { + p.destroy_sound(tree.sound_handle); + } + trees.remove_at(treeIndex); + } + + areaTreeIndices.resize(0); + for (uint i = 0; i < trees.length(); i++) { + if (trees[i].position >= areaStart && trees[i].position <= areaEnd) { + areaTreeIndices.insert_last(i); + } + } + } + + if (areaTreeIndices.length() == 2) { + Tree@ firstTree = trees[areaTreeIndices[0]]; + Tree@ secondTree = trees[areaTreeIndices[1]]; + int distance = firstTree.position - secondTree.position; + if (distance < 0) distance = -distance; + if (distance < TREE_MIN_DISTANCE) { + if (!place_tree_in_area(secondTree, areaStart, areaEnd)) { + place_tree_in_area(firstTree, areaStart, areaEnd); + } + } + } } } diff --git a/src/save_system.nvgt b/src/save_system.nvgt index e6d6022..cdfdb6d 100644 --- a/src/save_system.nvgt +++ b/src/save_system.nvgt @@ -291,6 +291,7 @@ void reset_game_state() { void start_new_game() { reset_game_state(); spawn_trees(5, 19); + normalize_tree_positions(); init_barricade(); init_time(); init_weather(); @@ -966,6 +967,7 @@ bool load_game_state() { tree.regen_timer.restart(); trees.insert_last(tree); } + normalize_tree_positions(); string[] snareData = get_string_list_or_split(saveData, "snares_data"); for (uint i = 0; i < snareData.length(); i++) { diff --git a/src/time_system.nvgt b/src/time_system.nvgt index 1d5e3a2..a6e2a11 100644 --- a/src/time_system.nvgt +++ b/src/time_system.nvgt @@ -104,15 +104,8 @@ void expand_regular_area() { notify("A " + width_desc + " stream flows through the new area at x " + actual_start + "."); } else { - // Try to place a tree with proper spacing - for (int attempt = 0; attempt < 20; attempt++) { - int tree_pos = random(new_start, new_end); - if (!tree_too_close(tree_pos)) { - Tree@ t = Tree(tree_pos); - trees.insert_last(t); - break; - } - } + // Try to place a tree with proper spacing and per-area limits + spawn_tree_in_area(new_start, new_end); } area_expanded_today = true; diff --git a/src/weather.nvgt b/src/weather.nvgt index 16586d3..e610a28 100644 --- a/src/weather.nvgt +++ b/src/weather.nvgt @@ -1,5 +1,6 @@ // Weather System // Provides ambient wind, rain, and thunder effects +// Tunable constants are in src/constants.nvgt // Weather states const int WEATHER_CLEAR = 0; @@ -13,40 +14,6 @@ const int INTENSITY_LOW = 1; const int INTENSITY_MEDIUM = 2; const int INTENSITY_HIGH = 3; -// Audio fade settings -const int WEATHER_FADE_DURATION = 8000; // 8 seconds for smooth transitions -const float WEATHER_MIN_VOLUME = -30.0; -const float WEATHER_MAX_VOLUME = 0.0; - -// Rain volume levels by intensity -const float RAIN_VOLUME_LIGHT = -18.0; -const float RAIN_VOLUME_MODERATE = -10.0; -const float RAIN_VOLUME_HEAVY = -3.0; - -// Wind gust settings -const int WIND_GUST_MIN_DELAY = 10000; // Min 10 seconds between gusts -const int WIND_GUST_MAX_DELAY = 25000; // Max 25 seconds between gusts - -// Thunder timing -const int THUNDER_MIN_INTERVAL = 8000; // Min 8 seconds between thunder -const int THUNDER_MAX_INTERVAL = 35000; // Max 35 seconds between thunder -const int THUNDER_MOVEMENT_SPEED = 2000; // ms per tile movement (slow roll across sky) -const float THUNDER_SOUND_VOLUME_STEP = 2.0; // Gentler volume falloff -const int THUNDER_SPAWN_DISTANCE_MIN = 20; // Min distance from player -const int THUNDER_SPAWN_DISTANCE_MAX = 40; // Max distance from player - -// Weather transition chances (out of 100) -const int CHANCE_CLEAR_TO_WINDY = 15; -const int CHANCE_CLEAR_TO_RAINY = 4; -const int CHANCE_CLEAR_TO_STORMY = 2; -const int CHANCE_WINDY_STAY = 55; -const int CHANCE_WINDY_TO_CLEAR = 25; -const int CHANCE_WINDY_TO_STORMY = 12; -const int CHANCE_RAINY_STAY = 45; -const int CHANCE_RAINY_TO_STORMY = 25; -const int CHANCE_STORMY_STAY = 40; -const int CHANCE_STORMY_TO_RAINY = 35; - // State variables int weather_state = WEATHER_CLEAR; int wind_intensity = INTENSITY_NONE;