// Centralized falling damage system // Safe fall height is 10 feet or less // Each foot above 10 has a chance to deal 0-4 damage // This means falling from great heights is VERY dangerous but not guaranteed fatal void apply_falling_damage(int fall_height) { // Always play the hit ground sound p.play_stationary("sounds/actions/hit_ground.ogg", false); if (fall_height <= SAFE_FALL_HEIGHT) { speak_with_history("Landed safely.", true); return; } // Calculate damage: roll 0-4 for each foot above 10 int damage = 0; for (int i = SAFE_FALL_HEIGHT; i < fall_height; i++) { damage += random(FALL_DAMAGE_MIN, FALL_DAMAGE_MAX); } // Apply damage player_health -= damage; if (player_health < 0) player_health = 0; // Feedback speak_with_history("Fell " + fall_height + " feet! Took " + damage + " damage. " + player_health + " health remaining.", true); } // Tree Object class Tree { int position; int sticks; int vines; int health; int height; // Height in feet int sound_handle; timer regen_timer; bool depleted; bool is_chopped; int minutes_since_depletion; // Track minutes for gradual replenishment Tree(int pos) { position = pos; depleted = false; is_chopped = false; sound_handle = -1; minutes_since_depletion = 0; refill(); } void refill() { sticks = random(2, 3); vines = random(1, 2); health = random(25, 40); height = random(8, 30); // Random height between 8 and 30 feet depleted = false; is_chopped = false; minutes_since_depletion = 0; } void respawn() { if (sound_handle != -1) { p.destroy_sound(sound_handle); sound_handle = -1; } 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(); } void update() { // Only play tree sound if not chopped and within 3 tiles distance if (!is_chopped) { int tree_distance = x - position; if (tree_distance < 0) tree_distance = -tree_distance; if (tree_distance <= TREE_SOUND_RANGE) { if (sound_handle == -1 || !p.sound_is_active(sound_handle)) { sound_handle = p.play_1d("sounds/environment/tree.ogg", x, position, true); if (sound_handle != -1) { p.update_sound_positioning_values(sound_handle, -1.0, TREE_SOUND_VOLUME_STEP, true); } } } else if (sound_handle != -1) { p.destroy_sound(sound_handle); sound_handle = -1; } } else if (sound_handle != -1) { p.destroy_sound(sound_handle); sound_handle = -1; } } void try_regen() { // Skip if tree is fully stocked if (!depleted && !is_chopped) return; // Check every minute (60000ms) if (regen_timer.elapsed < 60000) return; // Advance to next minute regen_timer.restart(); minutes_since_depletion++; if (is_chopped) { if (minutes_since_depletion >= 5) { respawn(); } return; } // At minute 5, completely refill if (minutes_since_depletion >= 5) { refill(); return; } // Skip if already fully stocked if (sticks >= 3 && vines >= 2) { depleted = false; is_chopped = false; return; } // Determine base chance based on minutes elapsed int base_chance = 0; if (minutes_since_depletion == 1) base_chance = 25; else if (minutes_since_depletion == 2) base_chance = 50; else if (minutes_since_depletion == 3) base_chance = 75; else if (minutes_since_depletion == 4) base_chance = 100; // Try to add items with decreasing probability int current_chance = base_chance; while (current_chance >= 25) { // Check if we can add anything if (sticks >= 3 && vines >= 2) break; // Roll for success int roll = random(1, 100); if (roll <= current_chance) { // Decide what to add (70% stick, 30% vine) int item_roll = random(1, 100); if (item_roll <= 70 && sticks < 3) { // Add stick sticks++; } else if (vines < 2) { // Add vine vines++; } else if (sticks < 3) { // Vine is full but stick isn't, add stick sticks++; } } else { // Failed roll, stop trying break; } // Reduce chance by 25% for next attempt (minimum 25%) current_chance -= 25; } // Mark as no longer depleted if we have at least one item if (sticks > 0 || vines > 0) { depleted = false; is_chopped = false; } } } Tree@[] trees; 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; } 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 < TREE_MIN_DISTANCE) { return true; } } return false; } 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(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 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); } } } } } void update_environment() { for(uint i = 0; i < trees.length(); i++) { trees[i].update(); trees[i].try_regen(); } } Tree@ get_tree_at(int target_x) { for(uint i=0; i 0 || vines_added > 0 || logs_added > 0) { string log_label = (logs_added == 1) ? " log" : " logs"; drop_message += " Got " + sticks_added + " sticks, " + vines_added + " vines, and " + logs_added + log_label + "."; } if (sticks_added < sticks_dropped || vines_added < vines_dropped || logs_added < 1) { drop_message += " Inventory full."; } p.play_stationary("sounds/items/stick.ogg", false); speak_with_history(drop_message, true); } } } } void perform_search(int current_x) { // First priority: Check for world drops on this tile or adjacent for (int check_x = current_x - 1; check_x <= current_x + 1; check_x++) { WorldDrop@ drop = get_drop_at(check_x); if (drop != null) { if (!try_pickup_world_drop(drop)) { return; } p.play_stationary("sounds/items/miscellaneous.ogg", false); remove_drop_at(check_x); return; } } // Check for snares nearby (adjacent within range) for (int check_x = current_x - SNARE_COLLECT_RANGE; check_x <= current_x + SNARE_COLLECT_RANGE; check_x++) { // Skip current x? User said "beside". If on top, it breaks. // But if I stand adjacent and shift... if (check_x == current_x) continue; // Safety against collecting own snare you stand on? (Collision happens on move) // Actually, collision happens when *moving onto* it. If you placed it, you are on it. // If active is false (just placed), you can pick it up. // If active is true (you moved away), moving back breaks it. // So checking adjacent is correct. WorldSnare@ s = get_snare_at(check_x); if (s != null) { if (s.has_catch) { if (inv_small_game >= get_personal_stack_limit()) { speak_with_history("You can't carry any more small game.", true); return; } if (inv_snares >= get_personal_stack_limit()) { speak_with_history("You can't carry any more snares.", true); return; } inv_small_game++; inv_small_game_types.insert_last(s.catch_type); inv_snares++; // Recover snare speak_with_history("Collected " + s.catch_type + " and snare.", true); } else { if (inv_snares >= get_personal_stack_limit()) { speak_with_history("You can't carry any more snares.", true); return; } inv_snares++; // Recover snare speak_with_history("Collected snare.", true); } p.play_stationary("sounds/items/miscellaneous.ogg", false); remove_snare_at(check_x); return; // Action taken, stop searching } } if (random(1, 100) <= 10) { speak_with_history("Found nothing.", true); return; } // Stream banks - Clay or reeds (within 3 tiles of stream, but not in water) bool near_stream_bank = false; for (uint i = 0; i < world_streams.length(); i++) { if (world_streams[i].contains_position(current_x)) { continue; } int center = world_streams[i].get_center_position(); int distance = center - current_x; if (distance < 0) distance = -distance; if (distance <= 3) { near_stream_bank = true; break; } } if (near_stream_bank) { bool found_reed = random(1, 100) <= 30; if (found_reed) { if (inv_reeds < get_personal_stack_limit()) { inv_reeds++; p.play_stationary("sounds/items/stick.ogg", false); speak_with_history("Found a reed.", true); return; } } else { if (inv_clay < get_personal_stack_limit()) { inv_clay++; p.play_stationary("sounds/items/clay.ogg", false); speak_with_history("Found clay.", true); return; } } if (!found_reed && inv_reeds < get_personal_stack_limit()) { inv_reeds++; p.play_stationary("sounds/items/stick.ogg", false); speak_with_history("Found a reed.", true); } else if (found_reed && inv_clay < get_personal_stack_limit()) { inv_clay++; p.play_stationary("sounds/items/clay.ogg", false); speak_with_history("Found clay.", true); } else if (found_reed) { speak_with_history("You can't carry any more reeds.", true); } else { speak_with_history("You can't carry any more clay.", true); } return; } // Trees (Sticks/Vines) - Check for nearby tree anywhere Tree@ nearest = null; int nearest_distance = 999; for(uint i=0; i 0 || nearest.vines > 0) { bool find_stick = (nearest.vines <= 0) || (nearest.sticks > 0 && random(0, 1) == 0); bool took_item = false; if (find_stick) { if (nearest.sticks > 0 && inv_sticks < get_personal_stack_limit()) { nearest.sticks--; inv_sticks++; p.play_stationary("sounds/items/stick.ogg", false); speak_with_history("Found a stick.", true); took_item = true; } else if (nearest.vines > 0 && inv_vines < get_personal_stack_limit()) { nearest.vines--; inv_vines++; p.play_stationary("sounds/items/vine.ogg", false); speak_with_history("Found a vine.", true); took_item = true; } } else { if (nearest.vines > 0 && inv_vines < get_personal_stack_limit()) { nearest.vines--; inv_vines++; p.play_stationary("sounds/items/vine.ogg", false); speak_with_history("Found a vine.", true); took_item = true; } else if (nearest.sticks > 0 && inv_sticks < get_personal_stack_limit()) { nearest.sticks--; inv_sticks++; p.play_stationary("sounds/items/stick.ogg", false); speak_with_history("Found a stick.", true); took_item = true; } } if (!took_item) { if (nearest.sticks > 0 && nearest.vines > 0) { speak_with_history("You can't carry any more sticks or vines.", true); } else if (nearest.sticks > 0) { speak_with_history("You can't carry any more sticks.", true); } else { speak_with_history("You can't carry any more vines.", true); } return; } if(nearest.sticks == 0 && nearest.vines == 0) { nearest.depleted = true; nearest.regen_timer.restart(); nearest.minutes_since_depletion = 0; } } else { speak_with_history("This area has nothing left.", true); } return; } // Stone terrain - check for stones bool is_stone_terrain = false; // Check base gravel area (20-34) if (current_x >= GRAVEL_START && current_x <= GRAVEL_END) { is_stone_terrain = true; } // Check expanded areas else if (expanded_area_start != -1 && current_x >= expanded_area_start && current_x <= expanded_area_end) { // Check for mountain terrain first MountainRange@ mountain = get_mountain_at(current_x); if (mountain !is null) { string terrain = mountain.get_terrain_at(current_x); if (terrain == "stone" || terrain == "hard_stone") { is_stone_terrain = true; } } else { // Regular expanded area - check terrain type int index = current_x - expanded_area_start; if (index >= 0 && index < int(expanded_terrain_types.length())) { string terrain = expanded_terrain_types[index]; // Handle "mountain:terrain" format from older saves if (terrain.find("mountain:") == 0) { terrain = terrain.substr(9); } if (terrain == "stone" || terrain == "hard_stone") { is_stone_terrain = true; } } } } if (is_stone_terrain) { if (inv_stones < get_personal_stack_limit()) { inv_stones++; p.play_stationary("sounds/items/stone.ogg", false); speak_with_history("Found a stone.", true); } else { speak_with_history("You can't carry any more stones.", true); } return; } speak_with_history("Found nothing.", true); } // Climbing functions void start_climbing_tree(int target_x) { Tree@ tree = get_tree_at(target_x); if (tree == null || tree.is_chopped) { return; } climbing = true; climb_target_y = tree.height; climb_timer.restart(); speak_with_history("Started climbing tree. Height is " + tree.height + " feet.", true); } void update_climbing() { if (!climbing) return; // Climb at 1 foot per 500ms if (climb_timer.elapsed > 500) { climb_timer.restart(); // Climbing up if (y < climb_target_y) { y++; p.play_stationary("sounds/actions/climb_tree.ogg", false); if (y >= climb_target_y) { climbing = false; speak_with_history("Reached the top at " + y + " feet.", true); } } // Climbing down else if (y > climb_target_y) { y--; p.play_stationary("sounds/actions/climb_tree.ogg", false); if (y <= 0) { climbing = false; y = 0; speak_with_history("Safely reached the ground.", true); } } } } void climb_down_tree() { if (y == 0 || climbing) return; climbing = true; climb_target_y = 0; climb_timer.restart(); speak_with_history("Climbing down.", true); } void start_falling() { if (y <= 0 || falling) return; falling = true; fall_start_y = y; // Remember where we started falling from fall_timer.restart(); // Start looping falling sound fall_sound_handle = p.play_stationary("sounds/actions/falling.ogg", true); } void update_falling() { if (!falling) return; // Get ground level (mountain elevation or 0) int ground_level = get_mountain_elevation_at(x); // Fall faster than climbing - 1 foot per 100ms if (fall_timer.elapsed > 100) { fall_timer.restart(); if (y > ground_level) { y--; // Restart falling sound with decreasing pitch each foot if (fall_sound_handle != -1) { p.destroy_sound(fall_sound_handle); } // Pitch ranges from 100 (high up) to 50 (near ground) float height_above_ground = float(y - ground_level); float pitch_percent = 50.0 + (50.0 * (height_above_ground / 30.0)); if (pitch_percent < 50.0) pitch_percent = 50.0; if (pitch_percent > 100.0) pitch_percent = 100.0; fall_sound_handle = p.play_stationary_extended("sounds/actions/falling.ogg", true, 0, 0, 0, pitch_percent); // Check if we've reached ground level if (y <= ground_level) { land_on_ground(ground_level); } } else { land_on_ground(ground_level); } } } void land_on_ground(int ground_level) { falling = false; // Stop falling sound if (fall_sound_handle != -1) { p.destroy_sound(fall_sound_handle); fall_sound_handle = -1; } // Calculate fall damage using centralized function (also plays hit_ground sound) int fall_height = fall_start_y - ground_level; y = ground_level; apply_falling_damage(fall_height); fall_start_y = 0; } // Mountain movement check bool can_move_mountain(int from_x, int to_x) { MountainRange@ mountain = get_mountain_at(to_x); if (mountain is null) { // Not entering a mountain return true; } // Check if from_x is also in same mountain if (!mountain.contains_position(from_x)) { // Entering mountain from edge - always allowed return true; } // Check elevation change if (mountain.is_steep_section(from_x, to_x)) { // Need rope if (inv_ropes < 1) { speak_with_history("You'll need a rope to climb there.", true); 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); } else { speak_with_history("Press down to climb down.", true); } // Store pending rope climb info pending_rope_climb_x = to_x; pending_rope_climb_elevation = mountain.get_elevation_at(to_x); return false; } return true; } // Rope climbing functions void start_rope_climb(bool climbing_up, int target_x, int target_elevation) { rope_climbing = true; rope_climb_target_x = target_x; rope_climb_target_y = target_elevation; rope_climb_start_y = y; rope_climb_timer.restart(); // Determine actual climbing direction based on elevation difference int elevation_diff = target_elevation - y; if (elevation_diff > 0) { rope_climb_up = true; } else if (elevation_diff < 0) { rope_climb_up = false; } else { // Already at target elevation, no climbing needed rope_climbing = false; return; } // Calculate distance to climb (use actual distance from current position) int distance = elevation_diff; if (distance < 0) distance = -distance; string direction = rope_climb_up ? "up" : "down"; speak_with_history("Climbing " + direction + ". " + distance + " feet.", true); } void update_rope_climbing() { if (!rope_climbing) return; // Check if we're already at the target (shouldn't happen, but safety check) if (y == rope_climb_target_y) { complete_rope_climb(); return; } // Climb at ROPE_CLIMB_SPEED ms per foot if (rope_climb_timer.elapsed > ROPE_CLIMB_SPEED) { rope_climb_timer.restart(); if (rope_climb_up) { // Climbing up if (y < rope_climb_target_y) { y++; // Check if we've reached the target BEFORE playing the sound if (y >= rope_climb_target_y) { complete_rope_climb(); } else { p.play_stationary("sounds/actions/climb_rope.ogg", false); } } } else { // Climbing down if (y > rope_climb_target_y) { y--; // Check if we've reached the target BEFORE playing the sound if (y <= rope_climb_target_y) { complete_rope_climb(); } else { p.play_stationary("sounds/actions/climb_rope.ogg", false); } } } } } void complete_rope_climb() { rope_climbing = false; x = rope_climb_target_x; y = rope_climb_target_y; // Play footstep for new terrain play_footstep(x, BASE_END, GRASS_END); speak_with_history("Reached elevation " + y + ".", true); } void check_rope_climb_fall() { if (!rope_climbing) return; if (key_down(KEY_LEFT) || key_down(KEY_RIGHT)) { // Fall from rope! rope_climbing = false; start_falling(); } }