937 lines
30 KiB
Plaintext
937 lines
30 KiB
Plaintext
// 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<trees.length(); i++) {
|
|
if(trees[i].position == target_x) {
|
|
return @trees[i];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
void damage_tree(int target_x, int damage) {
|
|
Tree@ target = null;
|
|
for(uint i=0; i<trees.length(); i++) {
|
|
if(trees[i].position == target_x) {
|
|
@target = @trees[i];
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(@target != null) {
|
|
if(!target.is_chopped) {
|
|
target.health -= damage;
|
|
p.play_stationary("sounds/weapons/axe_hit.ogg", false);
|
|
|
|
if(target.health <= 0) {
|
|
target.is_chopped = true;
|
|
target.depleted = true;
|
|
target.regen_timer.restart();
|
|
target.minutes_since_depletion = 0;
|
|
|
|
// Stop the looping sound
|
|
if (target.sound_handle != -1) {
|
|
p.destroy_sound(target.sound_handle);
|
|
target.sound_handle = -1;
|
|
}
|
|
|
|
// Play the falling sound at the tree's position
|
|
p.play_1d("sounds/items/tree.ogg", x, target.position, false);
|
|
|
|
int sticks_dropped = random(1, 3);
|
|
int vines_dropped = random(1, 2);
|
|
int sticks_added = add_to_stack(inv_sticks, sticks_dropped);
|
|
int vines_added = add_to_stack(inv_vines, vines_dropped);
|
|
int logs_added = add_to_stack(inv_logs, 1);
|
|
inv_sticks += sticks_added;
|
|
inv_vines += vines_added;
|
|
inv_logs += logs_added;
|
|
|
|
string drop_message = "Tree fell!";
|
|
if (sticks_added > 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<trees.length(); i++)
|
|
{
|
|
int distance = trees[i].position - current_x;
|
|
if (distance < 0) distance = -distance;
|
|
if(distance <= 1 && distance < nearest_distance)
|
|
{
|
|
nearest_distance = distance;
|
|
@nearest = @trees[i];
|
|
if (nearest_distance == 0) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(@nearest != null)
|
|
{
|
|
if(nearest.is_chopped) {
|
|
speak_with_history("This tree has been cut down.", true);
|
|
return;
|
|
}
|
|
|
|
if (nearest.depleted) {
|
|
speak_with_history("This tree is empty.", true);
|
|
return;
|
|
}
|
|
|
|
if(nearest.sticks > 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();
|
|
}
|
|
}
|