Sound has been updated. As a result snares can be collected from a bit further away as it is now harder to tell when you're about to step on it. A few string updates. Mountain terrain added. May require rope for traversal.

This commit is contained in:
Storm Dragon
2026-01-19 15:00:01 -05:00
parent 93dfd7347b
commit 5a16f798ac
12 changed files with 774 additions and 172 deletions
+79 -28
View File
@@ -1,4 +1,3 @@
#include "bgt_compat.nvgt"
#include "sound_pool.nvgt"
#include "virtual_dialogs.nvgt"
@@ -57,6 +56,10 @@ int run_main_menu() {
void main()
{
// Configure sound pool for better spatial audio
p.volume_step = AUDIO_VOLUME_STEP / float(AUDIO_TILE_SCALE); // Default falloff in audio units
p.pan_step = AUDIO_PAN_STEP; // Panning strength for scaled tile distances
show_window("Draugnorak");
bool game_started = false;
@@ -154,7 +157,12 @@ void main()
// Coordinates Key
if (key_pressed(KEY_X)) {
string direction_label = (facing == 1) ? "east" : "west";
screen_reader_speak(direction_label + ", x " + x + ", y " + y, true);
string terrain_info = "";
MountainRange@ mountain = get_mountain_at(x);
if (mountain !is null) {
terrain_info = ", elevation " + y + ", " + mountain.get_terrain_at(x);
}
screen_reader_speak(direction_label + ", x " + x + ", y " + y + terrain_info, true);
}
// Base Info Key (base only)
@@ -165,19 +173,41 @@ void main()
// Climbing and Falling Updates
update_climbing();
update_falling();
update_rope_climbing();
check_rope_climb_fall();
update_mountains();
// Down arrow to climb down from tree
// Down arrow to climb down from tree or start rope climb down
if (key_pressed(KEY_DOWN)) {
Tree@ tree = get_tree_at(x);
if (tree != null && !tree.is_chopped && y > 0 && !jumping && !climbing && !falling) {
climb_down_tree();
// Check for pending rope climb (going down)
if (pending_rope_climb_x != -1 && !rope_climbing && !climbing && !falling && !jumping) {
int elevation_change = pending_rope_climb_elevation - y;
if (elevation_change < 0) {
start_rope_climb(false, pending_rope_climb_x, pending_rope_climb_elevation);
pending_rope_climb_x = -1;
}
}
// Tree climbing down
else {
Tree@ tree = get_tree_at(x);
if (tree != null && !tree.is_chopped && y > 0 && !jumping && !climbing && !falling && !rope_climbing) {
climb_down_tree();
}
}
}
// Jumping Logic
if(key_pressed(KEY_UP))
{
if(!jumping && !climbing && !falling)
// Check for pending rope climb (going up)
if (pending_rope_climb_x != -1 && !rope_climbing && !climbing && !falling && !jumping) {
int elevation_change = pending_rope_climb_elevation - y;
if (elevation_change > 0) {
start_rope_climb(true, pending_rope_climb_x, pending_rope_climb_elevation);
pending_rope_climb_x = -1;
}
}
else if(!jumping && !climbing && !falling && !rope_climbing)
{
// Check if on tree tile
Tree@ tree = get_tree_at(x);
@@ -210,46 +240,67 @@ void main()
movetime = jumping ? jump_speed : walk_speed;
// Movement Logic
if (key_pressed(KEY_LEFT) && facing != 0 && !climbing && !falling) {
if (key_pressed(KEY_LEFT) && facing != 0 && !climbing && !falling && !rope_climbing) {
facing = 0;
screen_reader_speak("west", true);
walktimer.restart();
// Cancel pending rope climb when changing direction
pending_rope_climb_x = -1;
}
if (key_pressed(KEY_RIGHT) && facing != 1 && !climbing && !falling) {
if (key_pressed(KEY_RIGHT) && facing != 1 && !climbing && !falling && !rope_climbing) {
facing = 1;
screen_reader_speak("east", true);
walktimer.restart();
// Cancel pending rope climb when changing direction
pending_rope_climb_x = -1;
}
if(walktimer.elapsed > movetime)
{
int old_x = x;
// Check if trying to move left/right while in tree
if((key_down(KEY_LEFT) || key_down(KEY_RIGHT)) && y > 0 && !jumping && !falling) {
// Check if trying to move left/right while in tree (not in mountain)
MountainRange@ current_mountain = get_mountain_at(x);
if((key_down(KEY_LEFT) || key_down(KEY_RIGHT)) && y > 0 && !jumping && !falling && !rope_climbing && current_mountain is null) {
// Fall out of tree
climbing = false;
start_falling();
}
if(key_down(KEY_LEFT) && x > 0 && !climbing && !falling)
if(key_down(KEY_LEFT) && x > 0 && !climbing && !falling && !rope_climbing)
{
facing = 0;
x--;
walktimer.restart();
if(!jumping) {
play_footstep(x, BASE_END, GRASS_END);
check_snare_collision(x); // Check when moving onto a tile
// Check mountain movement
if (can_move_mountain(x, x - 1)) {
x--;
// Update elevation if in mountain
int new_elevation = get_mountain_elevation_at(x);
if (new_elevation != y && get_mountain_at(x) !is null) {
y = new_elevation;
}
walktimer.restart();
if(!jumping) {
play_footstep(x, BASE_END, GRASS_END);
check_snare_collision(x);
}
}
}
else if(key_down(KEY_RIGHT) && x < MAP_SIZE - 1 && !climbing && !falling)
else if(key_down(KEY_RIGHT) && x < MAP_SIZE - 1 && !climbing && !falling && !rope_climbing)
{
facing = 1;
x++;
walktimer.restart();
if(!jumping) {
play_footstep(x, BASE_END, GRASS_END);
check_snare_collision(x); // Check when moving onto a tile
// Check mountain movement
if (can_move_mountain(x, x + 1)) {
x++;
// Update elevation if in mountain
int new_elevation = get_mountain_elevation_at(x);
if (new_elevation != y && get_mountain_at(x) !is null) {
y = new_elevation;
}
walktimer.restart();
if(!jumping) {
play_footstep(x, BASE_END, GRASS_END);
check_snare_collision(x);
}
}
}
}
@@ -285,7 +336,7 @@ void main()
}
// Sling charge detection
if (sling_equipped && (key_down(KEY_LCONTROL) || key_down(KEY_RCONTROL)) && !sling_charging) {
if (sling_equipped && (key_down(KEY_LCTRL) || key_down(KEY_RCTRL)) && !sling_charging) {
if (inv_stones > 0) {
sling_charging = true;
sling_charge_timer.restart();
@@ -297,12 +348,12 @@ void main()
}
// Update sling charge state while holding
if (sling_charging && (key_down(KEY_LCONTROL) || key_down(KEY_RCONTROL))) {
if (sling_charging && (key_down(KEY_LCTRL) || key_down(KEY_RCTRL))) {
update_sling_charge();
}
// Sling release detection
if (sling_charging && (!key_down(KEY_LCONTROL) && !key_down(KEY_RCONTROL))) {
if (sling_charging && (!key_down(KEY_LCTRL) && !key_down(KEY_RCTRL))) {
release_sling_attack(x);
sling_charging = false;
if (sling_sound_handle != -1) {
@@ -317,7 +368,7 @@ void main()
if (spear_equipped) attack_cooldown = 800;
if (axe_equipped) attack_cooldown = 1600;
if((key_down(KEY_LCONTROL) || key_down(KEY_RCONTROL)) && attack_timer.elapsed > attack_cooldown)
if((key_down(KEY_LCTRL) || key_down(KEY_RCTRL)) && attack_timer.elapsed > attack_cooldown)
{
attack_timer.restart();
perform_attack(x);
@@ -325,6 +376,6 @@ void main()
}
// Audio Listener Update
p.update_listener_1d(x);
update_listener_tile(x);
}
}
Binary file not shown.
+55 -6
View File
@@ -1,7 +1,7 @@
string get_footstep_sound(int current_x, int base_end, int grass_end)
{
// Check if in water first (overrides all other terrain)
if (is_position_in_water(current_x)) {
// Check if in water first (regular streams or mountain streams)
if (is_position_in_water(current_x) || is_mountain_stream_at(current_x)) {
return "sounds/terrain/shallow_water.ogg";
}
@@ -27,17 +27,36 @@ string get_footstep_sound(int current_x, int base_end, int grass_end)
}
else if (expanded_area_start != -1 && current_x >= expanded_area_start && current_x <= expanded_area_end)
{
// Expanded area - check terrain type
// 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") {
return "sounds/terrain/stone.ogg";
} else if (terrain == "gravel") {
return "sounds/terrain/gravel.ogg";
} else if (terrain == "snow") {
return "sounds/terrain/snow.ogg";
}
}
// Regular expanded area - check terrain type
int index = current_x - expanded_area_start;
if (index >= 0 && index < expanded_terrain_types.length())
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") {
return "sounds/terrain/stone.ogg";
} else if (terrain == "grass") {
return "sounds/terrain/grass.ogg";
} else if (terrain == "snow") {
return "sounds/terrain/snow.ogg";
} else if (terrain == "gravel") {
return "sounds/terrain/gravel.ogg";
}
}
}
@@ -55,11 +74,41 @@ void play_footstep(int current_x, int base_end, int grass_end)
}
}
int to_audio_position(int tile_x)
{
return tile_x * AUDIO_TILE_SCALE;
}
float to_audio_volume_step(float volume_step)
{
return volume_step / float(AUDIO_TILE_SCALE);
}
int play_1d_tile(string sound_file, int listener_x, int sound_x, bool looping, bool persistent = false)
{
return p.play_1d(sound_file, to_audio_position(listener_x), to_audio_position(sound_x), looping, persistent);
}
bool update_sound_1d_tile(int slot, int sound_x)
{
return p.update_sound_1d(slot, to_audio_position(sound_x));
}
void update_listener_tile(int listener_x)
{
p.update_listener_1d(to_audio_position(listener_x));
}
void update_sound_range_1d_tile(int slot, int range_tiles)
{
p.update_sound_range_1d(slot, range_tiles * AUDIO_TILE_SCALE, range_tiles * AUDIO_TILE_SCALE);
}
int play_1d_with_volume_step(string sound_file, int listener_x, int sound_x, bool looping, float volume_step)
{
int slot = p.play_1d(sound_file, listener_x, sound_x, looping);
int slot = p.play_1d(sound_file, to_audio_position(listener_x), to_audio_position(sound_x), looping);
if (slot != -1) {
p.update_sound_positioning_values(slot, -1.0, volume_step, true);
p.update_sound_positioning_values(slot, -1.0, to_audio_volume_step(volume_step), true);
}
return slot;
}
+3 -3
View File
@@ -161,7 +161,7 @@ void release_sling_attack(int player_x) {
Tree@ tree = get_tree_at(check_x);
if (tree != null && !tree.is_chopped) {
// Stone hits tree but doesn't damage it
p.play_1d("sounds/weapons/sling_hit.ogg", player_x, check_x, false);
play_1d_tile("sounds/weapons/sling_hit.ogg", player_x, check_x, false);
screen_reader_speak("Stone hit tree at " + check_x + ".", true);
return;
}
@@ -179,11 +179,11 @@ void release_sling_attack(int player_x) {
// Damage the correct enemy type
if (hit_bandit) {
damage_bandit_at(target_x, damage);
p.play_1d("sounds/weapons/sling_hit.ogg", player_x, target_x, false);
play_1d_tile("sounds/weapons/sling_hit.ogg", player_x, target_x, false);
play_1d_with_volume_step("sounds/enemies/zombie_hit.ogg", player_x, target_x, false, BANDIT_SOUND_VOLUME_STEP);
} else {
damage_zombie_at(target_x, damage);
p.play_1d("sounds/weapons/sling_hit.ogg", player_x, target_x, false);
play_1d_tile("sounds/weapons/sling_hit.ogg", player_x, target_x, false);
play_1d_with_volume_step("sounds/enemies/zombie_hit.ogg", player_x, target_x, false, ZOMBIE_SOUND_VOLUME_STEP);
}
}
+29 -3
View File
@@ -87,9 +87,35 @@ const int BANDIT_DETECTION_RADIUS = 5;
const int BANDIT_WANDER_DIRECTION_CHANGE_MIN = 3000;
const int BANDIT_WANDER_DIRECTION_CHANGE_MAX = 8000;
// Stream audio
const int STREAM_SOUND_RANGE = 5;
const float STREAM_SOUND_VOLUME_STEP = 0.6;
// Audio ranges and volume falloff
// Formula: final_volume = start_volume - (volume_step × distance)
// Using 30 dB fade over range for gradual but noticeable falloff
const int AUDIO_TILE_SCALE = 10;
const float AUDIO_PAN_STEP = 2.0;
const float AUDIO_VOLUME_STEP = 3.0;
const int SNARE_SOUND_RANGE = 5;
const float SNARE_SOUND_VOLUME_STEP = 4.0; // More audible for locating snares
const float SNARE_SOUND_PAN_STEP = 4.0; // Stronger pan for direction
const int SNARE_COLLECT_RANGE = 2;
const int FIRE_SOUND_RANGE = 6;
const float FIRE_SOUND_VOLUME_STEP = 5.0; // 30 dB over 6 tiles
const int FIREPIT_SOUND_RANGE = 5;
const float FIREPIT_SOUND_VOLUME_STEP = 6.0; // 30 dB over 5 tiles
const int STREAM_SOUND_RANGE = 7;
const float STREAM_SOUND_VOLUME_STEP = 4.3; // 30 dB over 7 tiles
// Mountain configuration
const int MOUNTAIN_SIZE = 60;
const int MOUNTAIN_MIN_ELEVATION = 0;
const int MOUNTAIN_MAX_ELEVATION = 40;
const int MOUNTAIN_STEEP_THRESHOLD = 6;
const int MOUNTAIN_MAX_SLOPE = 20;
const int ROPE_CLIMB_SPEED = 1000;
const int MOUNTAIN_STREAM_SOUND_RANGE = 7;
const float MOUNTAIN_STREAM_VOLUME_STEP = 4.3; // 30 dB over 7 tiles
const int QUEST_MAX_ACTIVE = 4;
const int QUEST_CHANCE_PER_FAVOR = 10;
const int QUEST_MIN_CHANCE = 5;
+40 -39
View File
@@ -294,20 +294,21 @@ void run_barricade_menu() {
}
}
void simulate_crafting() {
void simulate_crafting(int item_count) {
screen_reader_speak("Crafting...", true);
timer t;
int duration = 4000;
int next_sound = 0;
while(t.elapsed < duration) {
if(t.elapsed > next_sound) {
float pitch = random(85, 115);
p.play_stationary_extended("sounds/crafting.ogg", false, 0, 0, 0, pitch);
next_sound = t.elapsed + 800;
// Nothing should take less than 4.
if(item_count < 4) {
item_count = 4;
}
for(int i = 0; i < item_count; i++) {
float pitch = random(85, 115);
p.play_stationary_extended("sounds/crafting.ogg", false, 0, 0, 0, pitch);
timer t;
while(t.elapsed < 800) {
wait(5);
menu_background_tick();
}
wait(5);
menu_background_tick();
}
p.play_stationary("sounds/crafting_complete.ogg", false);
}
@@ -321,7 +322,7 @@ void craft_knife() {
screen_reader_speak("You can't carry any more stone knives.", true);
return;
}
simulate_crafting();
simulate_crafting(2);
inv_stones -= 2;
inv_knives++;
screen_reader_speak("Crafted a Stone Knife.", true);
@@ -342,7 +343,7 @@ void craft_spear() {
screen_reader_speak("You can't carry any more spears.", true);
return;
}
simulate_crafting();
simulate_crafting(3);
inv_sticks--;
inv_vines--;
inv_stones--;
@@ -363,7 +364,7 @@ void craft_sling() {
screen_reader_speak("You can't carry any more slings.", true);
return;
}
simulate_crafting();
simulate_crafting(3);
inv_skins--;
inv_vines -= 2;
inv_slings++;
@@ -383,7 +384,7 @@ void craft_skin_hat() {
screen_reader_speak("You can't carry any more skin hats.", true);
return;
}
simulate_crafting();
simulate_crafting(2);
inv_skins--;
inv_vines--;
inv_skin_hats++;
@@ -403,7 +404,7 @@ void craft_skin_gloves() {
screen_reader_speak("You can't carry any more skin gloves.", true);
return;
}
simulate_crafting();
simulate_crafting(2);
inv_skins--;
inv_vines--;
inv_skin_gloves++;
@@ -423,7 +424,7 @@ void craft_skin_pants() {
screen_reader_speak("You can't carry any more skin pants.", true);
return;
}
simulate_crafting();
simulate_crafting(9);
inv_skins -= 6;
inv_vines -= 3;
inv_skin_pants++;
@@ -443,7 +444,7 @@ void craft_skin_tunic() {
screen_reader_speak("You can't carry any more skin tunics.", true);
return;
}
simulate_crafting();
simulate_crafting(6);
inv_skins -= 4;
inv_vines -= 2;
inv_skin_tunics++;
@@ -463,7 +464,7 @@ void craft_moccasins() {
screen_reader_speak("You can't carry any more moccasins.", true);
return;
}
simulate_crafting();
simulate_crafting(3);
inv_skins -= 2;
inv_vines--;
inv_moccasins++;
@@ -483,7 +484,7 @@ void craft_skin_pouch() {
screen_reader_speak("You can't carry any more skin pouches.", true);
return;
}
simulate_crafting();
simulate_crafting(3);
inv_skins -= 2;
inv_vines--;
inv_skin_pouches++;
@@ -503,7 +504,7 @@ void craft_snare() {
screen_reader_speak("You can't carry any more snares.", true);
return;
}
simulate_crafting();
simulate_crafting(3);
inv_sticks--;
inv_vines -= 2;
inv_snares++;
@@ -525,7 +526,7 @@ void craft_axe() {
screen_reader_speak("You can't carry any more stone axes.", true);
return;
}
simulate_crafting();
simulate_crafting(4);
inv_sticks--;
inv_vines--;
inv_stones -= 2;
@@ -547,7 +548,7 @@ void craft_firepit() {
if (inv_stones < 9) missing += "9 stones ";
if (missing == "") {
simulate_crafting();
simulate_crafting(9);
inv_stones -= 9;
add_world_firepit(x);
screen_reader_speak("Firepit built here.", true);
@@ -569,7 +570,7 @@ void craft_campfire() {
if (inv_sticks < 2) missing += "2 sticks ";
if (missing == "") {
simulate_crafting();
simulate_crafting(3);
inv_logs--;
inv_sticks -= 2;
// Build the fire at the firepit location, not player location
@@ -599,7 +600,7 @@ void craft_herb_garden() {
if (inv_logs < 2) missing += "2 logs ";
if (missing == "") {
simulate_crafting();
simulate_crafting(14);
inv_stones -= 9;
inv_vines -= 3;
inv_logs -= 2;
@@ -625,7 +626,7 @@ void craft_storage() {
if (inv_vines < STORAGE_VINE_COST) missing += STORAGE_VINE_COST + " vines ";
if (missing == "") {
simulate_crafting();
simulate_crafting(23);
inv_logs -= STORAGE_LOG_COST;
inv_stones -= STORAGE_STONE_COST;
inv_vines -= STORAGE_VINE_COST;
@@ -650,7 +651,7 @@ void craft_pasture() {
if (inv_vines < PASTURE_VINE_COST) missing += PASTURE_VINE_COST + " vines ";
if (missing == "") {
simulate_crafting();
simulate_crafting(28);
inv_logs -= PASTURE_LOG_COST;
inv_vines -= PASTURE_VINE_COST;
add_world_pasture(x);
@@ -675,7 +676,7 @@ void craft_stable() {
if (inv_vines < STABLE_VINE_COST) missing += STABLE_VINE_COST + " vines ";
if (missing == "") {
simulate_crafting();
simulate_crafting(35);
inv_logs -= STABLE_LOG_COST;
inv_stones -= STABLE_STONE_COST;
inv_vines -= STABLE_VINE_COST;
@@ -700,7 +701,7 @@ void craft_altar() {
if (inv_sticks < ALTAR_STICK_COST) missing += ALTAR_STICK_COST + " sticks ";
if (missing == "") {
simulate_crafting();
simulate_crafting(12);
inv_stones -= ALTAR_STONE_COST;
inv_sticks -= ALTAR_STICK_COST;
add_world_altar(x);
@@ -720,7 +721,7 @@ void reinforce_barricade_with_sticks() {
return;
}
simulate_crafting();
simulate_crafting(BARRICADE_STICK_COST);
inv_sticks -= BARRICADE_STICK_COST;
int gained = add_barricade_health(BARRICADE_STICK_HEALTH);
screen_reader_speak("Reinforced barricade with sticks. +" + gained + " health. Now " + barricade_health + " of " + BARRICADE_MAX_HEALTH + ".", true);
@@ -736,7 +737,7 @@ void reinforce_barricade_with_vines() {
return;
}
simulate_crafting();
simulate_crafting(BARRICADE_VINE_COST);
inv_vines -= BARRICADE_VINE_COST;
int gained = add_barricade_health(BARRICADE_VINE_HEALTH);
screen_reader_speak("Reinforced barricade with vines. +" + gained + " health. Now " + barricade_health + " of " + BARRICADE_MAX_HEALTH + ".", true);
@@ -752,7 +753,7 @@ void reinforce_barricade_with_log() {
return;
}
simulate_crafting();
simulate_crafting(BARRICADE_LOG_COST);
inv_logs -= BARRICADE_LOG_COST;
int gained = add_barricade_health(BARRICADE_LOG_HEALTH);
screen_reader_speak("Reinforced barricade with log. +" + gained + " health. Now " + barricade_health + " of " + BARRICADE_MAX_HEALTH + ".", true);
@@ -768,7 +769,7 @@ void reinforce_barricade_with_stones() {
return;
}
simulate_crafting();
simulate_crafting(BARRICADE_STONE_COST);
inv_stones -= BARRICADE_STONE_COST;
int gained = add_barricade_health(BARRICADE_STONE_HEALTH);
screen_reader_speak("Reinforced barricade with stones. +" + gained + " health. Now " + barricade_health + " of " + BARRICADE_MAX_HEALTH + ".", true);
@@ -784,7 +785,7 @@ void craft_fishing_pole() {
screen_reader_speak("You can't carry any more fishing poles.", true);
return;
}
simulate_crafting();
simulate_crafting(3);
inv_sticks--;
inv_vines -= 2;
inv_fishing_poles++;
@@ -803,7 +804,7 @@ void craft_rope() {
screen_reader_speak("You can't carry any more rope.", true);
return;
}
simulate_crafting();
simulate_crafting(3);
inv_vines -= 3;
inv_ropes++;
screen_reader_speak("Crafted rope.", true);
@@ -821,7 +822,7 @@ void craft_reed_basket() {
screen_reader_speak("You can't carry any more reed baskets.", true);
return;
}
simulate_crafting();
simulate_crafting(3);
inv_reeds -= 3;
inv_reed_baskets++;
screen_reader_speak("Crafted a reed basket.", true);
@@ -846,7 +847,7 @@ void craft_clay_pot() {
screen_reader_speak("You can't carry any more clay pots.", true);
return;
}
simulate_crafting();
simulate_crafting(3);
inv_clay -= 3;
inv_clay_pots++;
screen_reader_speak("Crafted a clay pot.", true);
@@ -880,7 +881,7 @@ void butcher_small_game() {
screen_reader_speak("You can't carry any more skins.", true);
return;
}
simulate_crafting();
simulate_crafting(1);
// Get the type of game we're butchering (first in the list)
string game_type = inv_small_game_types[0];
+157 -43
View File
@@ -40,18 +40,14 @@ class Tree {
}
void update() {
// Only play tree sound if not chopped and within 3 tiles distance (2 tiles on either side)
// Keep tree sound active so distance-based fade can work.
if (!is_chopped) {
if (abs(x - position) <= 3) {
if (sound_handle == -1 || !p.sound_is_active(sound_handle)) {
sound_handle = p.play_1d("sounds/environment/tree.ogg", x, position, true);
}
} else {
if (sound_handle != -1) {
p.destroy_sound(sound_handle);
sound_handle = -1;
}
if (sound_handle == -1 || !p.sound_is_active(sound_handle)) {
sound_handle = play_1d_tile("sounds/environment/tree.ogg", x, position, true);
}
} else if (sound_handle != -1) {
p.destroy_sound(sound_handle);
sound_handle = -1;
}
}
@@ -199,7 +195,7 @@ void damage_tree(int target_x, int damage) {
}
// Play the falling sound at the tree's position
p.play_1d("sounds/items/tree.ogg", x, target.position, false);
play_1d_tile("sounds/items/tree.ogg", x, target.position, false);
int sticks_dropped = random(1, 3);
int vines_dropped = random(1, 2);
@@ -228,10 +224,8 @@ void damage_tree(int target_x, int damage) {
void perform_search(int current_x)
{
// Check for Snares nearby (Current or Adjacent)
// "Shift beside the snare will collect the snare" -> adjacent
// We check current and +/- 1
for (int check_x = current_x - 1; check_x <= current_x + 1; check_x++) {
// 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)
@@ -496,11 +490,14 @@ void start_falling() {
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 > 0) {
if (y > ground_level) {
y--;
// Restart falling sound with decreasing pitch each foot
@@ -509,38 +506,155 @@ void update_falling() {
}
// Pitch ranges from 100 (high up) to 50 (near ground)
// Calculate based on current y position
float pitch_percent = 50.0 + (50.0 * (y / 30.0));
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 {
// Hit the ground
falling = false;
// Stop falling sound
if (fall_sound_handle != -1) {
p.destroy_sound(fall_sound_handle);
fall_sound_handle = -1;
}
p.play_stationary("sounds/actions/hit_ground.ogg", false);
// Calculate fall damage
int fall_height = fall_start_y;
if (fall_height > 10) {
int damage = 0;
for (int i = 10; i < fall_height; i++) {
damage += random(1, 3);
}
player_health -= damage;
screen_reader_speak("Fell " + fall_height + " feet! Took " + damage + " damage. " + player_health + " health remaining.", true);
} else {
screen_reader_speak("Landed safely.", true);
}
fall_start_y = 0;
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;
}
p.play_stationary("sounds/actions/hit_ground.ogg", false);
// Calculate fall damage
int fall_height = fall_start_y - ground_level;
y = ground_level;
if (fall_height > 10) {
int damage = 0;
for (int i = 10; i < fall_height; i++) {
damage += random(1, 3);
}
player_health -= damage;
screen_reader_speak("Fell " + fall_height + " feet! Took " + damage + " damage. " + player_health + " health remaining.", true);
} else {
screen_reader_speak("Landed safely.", true);
}
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) {
screen_reader_speak("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) {
screen_reader_speak("Press up to climb up.", true);
} else {
screen_reader_speak("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_up = climbing_up;
rope_climb_target_x = target_x;
rope_climb_target_y = target_elevation;
rope_climb_start_y = y;
rope_climb_timer.restart();
int distance = rope_climb_target_y - y;
if (distance < 0) distance = -distance;
string direction = climbing_up ? "up" : "down";
screen_reader_speak("Climbing " + direction + ". " + distance + " feet.", true);
}
void update_rope_climbing() {
if (!rope_climbing) 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++;
p.play_stationary("sounds/actions/climb_rope.ogg", false);
if (y >= rope_climb_target_y) {
complete_rope_climb();
}
}
} else {
// Climbing down
if (y > rope_climb_target_y) {
y--;
p.play_stationary("sounds/actions/climb_rope.ogg", false);
if (y <= rope_climb_target_y) {
complete_rope_climb();
}
}
}
}
}
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);
screen_reader_speak("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();
}
}
+2 -2
View File
@@ -40,7 +40,7 @@ void update_notifications() {
void check_notification_keys() {
// [ for previous notification (older) with position
if (key_pressed(KEY_LBRACKET)) {
if (key_pressed(KEY_LEFTBRACKET)) {
if (notification_history.length() == 0) {
screen_reader_speak("No notifications.", true);
return;
@@ -58,7 +58,7 @@ void check_notification_keys() {
}
// ] for next notification (newer) with position
if (key_pressed(KEY_RBRACKET)) {
if (key_pressed(KEY_RIGHTBRACKET)) {
if (notification_history.length() == 0) {
screen_reader_speak("No notifications.", true);
return;
+10
View File
@@ -11,6 +11,16 @@ int fall_sound_handle = -1; // Handle for looping fall sound
timer fall_timer; // For fall sound pitch
timer climb_timer; // For climb speed
// Rope climbing state
bool rope_climbing = false;
bool rope_climb_up = true;
int rope_climb_target_x = 0;
int rope_climb_target_y = 0;
int rope_climb_start_y = 0;
timer rope_climb_timer;
int pending_rope_climb_x = -1;
int pending_rope_climb_elevation = 0;
// Health System
int player_health = 10;
int base_max_health = 10;
+79
View File
@@ -145,6 +145,7 @@ void clear_world_objects() {
clear_zombies();
clear_bandits();
clear_mountains();
}
void reset_game_state() {
@@ -161,6 +162,13 @@ void reset_game_state() {
fall_start_y = 0;
sling_charging = false;
searching = false;
rope_climbing = false;
rope_climb_up = true;
rope_climb_target_x = 0;
rope_climb_target_y = 0;
rope_climb_start_y = 0;
pending_rope_climb_x = -1;
pending_rope_climb_elevation = 0;
player_health = 10;
base_max_health = 10;
@@ -302,6 +310,32 @@ string serialize_bandit(Bandit@ bandit) {
return bandit.position + "|" + bandit.health + "|" + bandit.weapon_type + "|" + bandit.behavior_state + "|" + bandit.wander_direction + "|" + bandit.move_interval;
}
string serialize_mountain(MountainRange@ mountain) {
string result = mountain.start_position + "|" + mountain.end_position + "|";
// Serialize elevations
for (int i = 0; i < int(mountain.elevations.length()); i++) {
if (i > 0) result += ",";
result += mountain.elevations[i];
}
result += "|";
// Serialize terrain types
for (int i = 0; i < int(mountain.terrain_types.length()); i++) {
if (i > 0) result += ",";
result += mountain.terrain_types[i];
}
result += "|";
// Serialize stream positions
for (uint i = 0; i < mountain.stream_positions.length(); i++) {
if (i > 0) result += ",";
result += mountain.stream_positions[i];
}
return result;
}
string join_string_array(const string[]@ arr) {
if (@arr == null || arr.length() == 0) return "";
string result = arr[0];
@@ -645,6 +679,12 @@ bool save_game_state() {
}
saveData.set("bandits_data", join_string_array(banditData));
string[] mountainData;
for (uint i = 0; i < world_mountains.length(); i++) {
mountainData.insert_last(serialize_mountain(world_mountains[i]));
}
saveData.set("mountains_data", join_string_array(mountainData));
string rawData = saveData.serialize();
string encryptedData = encrypt_save_data(rawData);
return save_data(SAVE_FILE_PATH, encryptedData);
@@ -982,6 +1022,45 @@ bool load_game_state() {
bandits.insert_last(b);
}
string[] mountainData = get_string_list_or_split(saveData, "mountains_data");
for (uint i = 0; i < mountainData.length(); i++) {
string[]@ parts = mountainData[i].split("|");
if (parts.length() < 5) continue;
int start_pos = parse_int(parts[0]);
int end_pos = parse_int(parts[1]);
int size = end_pos - start_pos + 1;
// Create mountain with minimal init (we'll override everything)
MountainRange@ mountain = MountainRange(start_pos, 1);
mountain.start_position = start_pos;
mountain.end_position = end_pos;
// Parse elevations
string[]@ elev_parts = parts[2].split(",");
mountain.elevations.resize(elev_parts.length());
for (uint j = 0; j < elev_parts.length(); j++) {
mountain.elevations[j] = parse_int(elev_parts[j]);
}
// Parse terrain types
string[]@ terrain_parts = parts[3].split(",");
mountain.terrain_types.resize(terrain_parts.length());
for (uint j = 0; j < terrain_parts.length(); j++) {
mountain.terrain_types[j] = terrain_parts[j];
}
// Parse stream positions
if (parts[4].length() > 0) {
string[]@ stream_parts = parts[4].split(",");
for (uint j = 0; j < stream_parts.length(); j++) {
mountain.stream_positions.insert_last(parse_int(stream_parts[j]));
}
}
world_mountains.insert_last(mountain);
}
update_ambience(true);
return true;
}
+66 -17
View File
@@ -17,7 +17,7 @@ bool crossfade_active = false;
bool crossfade_to_night = false; // true = fading to night, false = fading to day
timer crossfade_timer;
const int CROSSFADE_DURATION = 60000; // 1 minute (1 game hour)
const float CROSSFADE_MIN_VOLUME = -40.0; // dB, effectively silent but not extreme
const float CROSSFADE_MIN_VOLUME = -25.0; // dB, keep overlap audible during crossfade
const float CROSSFADE_MAX_VOLUME = 0.0; // dB, full volume
// Expansion and invasion tracking
@@ -53,6 +53,16 @@ void expand_area() {
// Play invasion sound
p.play_stationary("sounds/enemies/invasion.ogg", false);
// 25% chance for mountain, 75% for regular expansion
int type_roll = random(0, 3);
if (type_roll == 0) {
expand_mountain();
} else {
expand_regular_area();
}
}
void expand_regular_area() {
// Calculate new area
int new_start = MAP_SIZE;
int new_end = MAP_SIZE + EXPANSION_SIZE - 1;
@@ -103,6 +113,30 @@ void expand_area() {
notify("The area has expanded! New territory discovered to the east.");
}
void expand_mountain() {
int new_start = MAP_SIZE;
int size = MOUNTAIN_SIZE;
int new_end = new_start + size - 1;
if (expanded_area_start == -1) {
expanded_area_start = new_start;
}
expanded_area_end = new_end;
MAP_SIZE += size;
// Generate mountain range
MountainRange@ mountain = MountainRange(new_start, size);
world_mountains.insert_last(mountain);
// Fill terrain types array for compatibility with save system
for (int i = 0; i < size; i++) {
expanded_terrain_types.insert_last("mountain:" + mountain.terrain_types[i]);
}
area_expanded_today = true;
notify("A mountain range has been discovered to the east!");
}
void start_invasion() {
expand_area();
invasion_active = true;
@@ -139,26 +173,34 @@ void schedule_invasion() {
}
void check_scheduled_invasion() {
if (invasion_active || invasion_triggered_today) return;
if (invasion_scheduled_hour == -1) return;
if (current_hour == invasion_scheduled_hour) {
invasion_scheduled_hour = -1;
invasion_triggered_today = true;
start_invasion();
} else if (current_hour > 11) {
invasion_scheduled_hour = -1;
if (invasion_active) return;
// Check scheduled invasion regardless of triggered flag (fixes bug where flag was set early in old saves)
if (invasion_scheduled_hour != -1) {
if (current_hour == invasion_scheduled_hour) {
invasion_scheduled_hour = -1;
invasion_triggered_today = true;
start_invasion();
} else if (current_hour > 11) {
invasion_scheduled_hour = -1;
}
return;
}
if (invasion_triggered_today) return;
}
void attempt_daily_invasion() {
if (current_day < 2) return;
if (invasion_triggered_today || invasion_active) return;
if (invasion_roll_done_today) return;
if (current_hour < 6 || current_hour > 12) return;
invasion_roll_done_today = true;
int roll = random(1, 100);
if (roll > invasion_chance) return;
invasion_triggered_today = true;
schedule_invasion();
check_scheduled_invasion();
}
@@ -250,24 +292,30 @@ void attempt_blessing() {
favor -= 1.0;
if (favor < 0) favor = 0;
string[] god_names = {
"Odin's", "Thor's", "Freyja's", "Loki's", "Tyr's", "Baldur's",
"Frigg's", "Heimdall's", "Hel's", "Fenrir's", "Freyr's", "The gods'"
};
string god_name = god_names[random(0, god_names.length() - 1)];
if (choice == 0) {
int before = player_health;
player_health += BLESSING_HEAL_AMOUNT;
if (player_health > max_health) player_health = max_health;
int healed = player_health - before;
string bonus = (healed > 0) ? "You feel restored. +" + healed + " health." : "You feel restored.";
notify("The gods' favor shines upon you. " + bonus);
notify(god_name + " favor shines upon you. " + bonus);
} else if (choice == 1) {
blessing_speed_active = true;
blessing_speed_timer.restart();
update_max_health_from_equipment();
notify("The gods' favor shines upon you. You feel swift for a while.");
notify(god_name + " favor shines upon you. You feel swift for a while.");
} else if (choice == 2) {
int gained = add_barricade_health(BLESSING_BARRICADE_REPAIR);
string bonus = (gained > 0)
? "A divine force repairs the barricade. +" + gained + " health."
: "A divine force surrounds the barricade.";
notify("The gods' favor shines upon you. " + bonus);
notify(god_name + " favor shines upon you. " + bonus);
}
}
@@ -393,10 +441,11 @@ void update_crossfade() {
float progress = float(crossfade_timer.elapsed) / float(CROSSFADE_DURATION);
if (progress > 1.0) progress = 1.0;
// Volume interpolation: fade out goes 0 -> -40, fade in goes -40 -> 0
float volume_range = CROSSFADE_MAX_VOLUME - CROSSFADE_MIN_VOLUME; // 40 dB range
float fade_out_vol = CROSSFADE_MAX_VOLUME - (volume_range * progress); // 0 -> -40
float fade_in_vol = CROSSFADE_MIN_VOLUME + (volume_range * progress); // -40 -> 0
// Volume interpolation: use a slow-start curve to make fade-outs more gradual
float volume_range = CROSSFADE_MAX_VOLUME - CROSSFADE_MIN_VOLUME; // dB range
float eased_progress = progress * progress;
float fade_out_vol = CROSSFADE_MAX_VOLUME - (volume_range * eased_progress); // 0 -> min
float fade_in_vol = CROSSFADE_MIN_VOLUME + (volume_range * eased_progress); // min -> 0
if (crossfade_to_night) {
// Fading day out, night in
+251 -31
View File
@@ -114,15 +114,11 @@ class WorldSnare {
minute_timer.restart();
}
// Limit snare sound to 2 tiles distance
if (abs(x - position) <= 2) {
if (sound_handle == -1 || !p.sound_is_active(sound_handle)) {
sound_handle = p.play_1d("sounds/actions/set_snare.ogg", x, position, true);
}
} else {
// Keep snare sound active so distance-based fade can work.
if (sound_handle == -1 || !p.sound_is_active(sound_handle)) {
sound_handle = play_1d_tile("sounds/actions/set_snare.ogg", x, position, true);
if (sound_handle != -1) {
p.destroy_sound(sound_handle);
sound_handle = -1;
p.update_sound_positioning_values(sound_handle, SNARE_SOUND_PAN_STEP, to_audio_volume_step(SNARE_SOUND_VOLUME_STEP), true);
}
}
@@ -200,13 +196,13 @@ class WorldFire {
// Warn when fuel is low (30 seconds remaining)
if (!low_fuel_warned && fuel_remaining <= 30000 && fuel_remaining > 0) {
low_fuel_warned = true;
notify("Fire at " + position + " is getting low!");
notify("Fire at x " + position + " y " + y + " is getting low!");
}
// Fire went out
if (fuel_remaining <= 0) {
fuel_remaining = 0;
notify("Fire at " + position + " has gone out.");
notify("Fire at x " + position + " y " + y + " has gone out.");
if (sound_handle != -1) {
p.destroy_sound(sound_handle);
sound_handle = -1;
@@ -215,16 +211,12 @@ class WorldFire {
}
}
// Limit fire sound to 2 tiles distance (only if burning)
// Keep fire sound active while burning so distance-based fade can work.
if (is_burning()) {
if (abs(x - position) <= 2) {
if (sound_handle == -1 || !p.sound_is_active(sound_handle)) {
sound_handle = p.play_1d("sounds/items/fire.ogg", x, position, true);
}
} else {
if (sound_handle == -1 || !p.sound_is_active(sound_handle)) {
sound_handle = play_1d_tile("sounds/items/fire.ogg", x, position, true);
if (sound_handle != -1) {
p.destroy_sound(sound_handle);
sound_handle = -1;
p.update_sound_positioning_values(sound_handle, -1.0, to_audio_volume_step(FIRE_SOUND_VOLUME_STEP), true);
}
}
}
@@ -297,11 +289,13 @@ class WorldStream {
int start_position;
int end_position;
int sound_handle;
int sound_position;
WorldStream(int start_pos, int width) {
start_position = start_pos;
end_position = start_pos + width - 1;
sound_handle = -1;
sound_position = -1;
}
bool contains_position(int pos) {
@@ -317,22 +311,25 @@ class WorldStream {
}
void update() {
int center = get_center_position();
// Play stream sound within range of center
if (abs(x - center) <= STREAM_SOUND_RANGE) {
if (sound_handle == -1 || !p.sound_is_active(sound_handle)) {
sound_handle = p.play_1d("sounds/terrain/stream.ogg", x, center, true);
if (sound_handle != -1) {
p.update_sound_positioning_values(sound_handle, -1.0, STREAM_SOUND_VOLUME_STEP, true);
p.update_sound_range_1d(sound_handle, STREAM_SOUND_RANGE, STREAM_SOUND_RANGE);
}
}
int sound_pos = 0;
if (x < start_position) {
sound_pos = start_position;
} else if (x > end_position) {
sound_pos = end_position;
} else {
sound_pos = x;
}
// Keep stream sound active so distance-based fade can work.
if (sound_handle == -1 || !p.sound_is_active(sound_handle)) {
sound_handle = play_1d_tile("sounds/terrain/stream.ogg", x, sound_pos, true);
sound_position = sound_pos;
if (sound_handle != -1) {
p.destroy_sound(sound_handle);
sound_handle = -1;
p.update_sound_positioning_values(sound_handle, -1.0, to_audio_volume_step(STREAM_SOUND_VOLUME_STEP), true);
}
} else if (sound_position != sound_pos) {
update_sound_1d_tile(sound_handle, sound_pos);
sound_position = sound_pos;
}
}
@@ -341,6 +338,7 @@ class WorldStream {
p.destroy_sound(sound_handle);
sound_handle = -1;
}
sound_position = -1;
}
}
WorldStream@[] world_streams;
@@ -1021,3 +1019,225 @@ WorldStream@ get_stream_at(int pos) {
bool is_position_in_water(int pos) {
return get_stream_at(pos) != null;
}
// Mountain Range Class
class MountainRange {
int start_position;
int end_position;
int[] elevations;
string[] terrain_types;
int[] stream_positions;
int stream_sound_handle;
int stream_sound_position;
MountainRange(int start_pos, int size) {
start_position = start_pos;
end_position = start_pos + size - 1;
elevations.resize(size);
terrain_types.resize(size);
stream_sound_handle = -1;
stream_sound_position = -1;
generate_terrain();
}
void generate_terrain() {
int size = int(elevations.length());
// Initialize endpoints at moderate elevations
elevations[0] = random(5, 15);
elevations[size - 1] = random(5, 15);
// Use midpoint displacement for natural terrain
midpoint_displace(0, size - 1, 25);
// Clamp values to valid range
for (int i = 0; i < size; i++) {
if (elevations[i] < MOUNTAIN_MIN_ELEVATION) elevations[i] = MOUNTAIN_MIN_ELEVATION;
if (elevations[i] > MOUNTAIN_MAX_ELEVATION) elevations[i] = MOUNTAIN_MAX_ELEVATION;
}
// Smooth to enforce max slope constraint
smooth_slopes();
// Assign terrain types based on elevation
for (int i = 0; i < size; i++) {
if (elevations[i] > 30) {
terrain_types[i] = "snow";
} else if (elevations[i] > 15) {
terrain_types[i] = "stone";
} else {
terrain_types[i] = "gravel";
}
}
// Place streams in valley areas
place_streams();
}
void midpoint_displace(int left, int right, int roughness) {
if (right - left <= 1) return;
int mid = (left + right) / 2;
int avg = (elevations[left] + elevations[right]) / 2;
int displacement = random(-roughness, roughness);
elevations[mid] = avg + displacement;
int new_roughness = roughness * 7 / 10;
if (new_roughness < 1) new_roughness = 1;
midpoint_displace(left, mid, new_roughness);
midpoint_displace(mid, right, new_roughness);
}
void smooth_slopes() {
bool changed = true;
int iterations = 0;
while (changed && iterations < 100) {
changed = false;
iterations++;
for (int i = 1; i < int(elevations.length()); i++) {
int diff = elevations[i] - elevations[i-1];
if (diff > MOUNTAIN_MAX_SLOPE) {
elevations[i] = elevations[i-1] + MOUNTAIN_MAX_SLOPE;
changed = true;
} else if (diff < -MOUNTAIN_MAX_SLOPE) {
elevations[i] = elevations[i-1] - MOUNTAIN_MAX_SLOPE;
changed = true;
}
}
}
}
void place_streams() {
// Find valley bottoms (local minima)
int[] valleys;
for (int i = 2; i < int(elevations.length()) - 2; i++) {
if (elevations[i] < elevations[i-1] && elevations[i] < elevations[i+1] &&
elevations[i] < elevations[i-2] && elevations[i] < elevations[i+2]) {
valleys.insert_last(i);
}
}
// Place 1-3 streams in valleys
int num_streams = random(1, 3);
if (num_streams > int(valleys.length())) num_streams = int(valleys.length());
for (int i = 0; i < num_streams && valleys.length() > 0; i++) {
int idx = random(0, int(valleys.length()) - 1);
stream_positions.insert_last(valleys[idx]);
valleys.remove_at(idx);
}
}
int get_elevation_at(int world_x) {
if (world_x < start_position || world_x > end_position) return 0;
int index = world_x - start_position;
return elevations[index];
}
string get_terrain_at(int world_x) {
if (world_x < start_position || world_x > end_position) return "stone";
int index = world_x - start_position;
return terrain_types[index];
}
int get_elevation_change(int from_x, int to_x) {
int from_elev = get_elevation_at(from_x);
int to_elev = get_elevation_at(to_x);
return to_elev - from_elev;
}
bool is_steep_section(int from_x, int to_x) {
int change = get_elevation_change(from_x, to_x);
if (change < 0) change = -change;
return change >= MOUNTAIN_STEEP_THRESHOLD;
}
bool contains_position(int world_x) {
return world_x >= start_position && world_x <= end_position;
}
bool is_stream_at(int world_x) {
if (!contains_position(world_x)) return false;
int index = world_x - start_position;
for (uint i = 0; i < stream_positions.length(); i++) {
if (stream_positions[i] == index) return true;
}
return false;
}
void update() {
if (stream_positions.length() == 0) return;
// Find nearest stream to player
int nearest_stream = -1;
int nearest_distance = 999;
for (uint i = 0; i < stream_positions.length(); i++) {
int stream_world_x = start_position + stream_positions[i];
int distance = abs(x - stream_world_x);
if (distance < nearest_distance) {
nearest_distance = distance;
nearest_stream = stream_world_x;
}
}
// Keep nearest stream sound active so distance-based fade can work.
if (nearest_stream != -1) {
if (stream_sound_handle == -1 || !p.sound_is_active(stream_sound_handle)) {
stream_sound_handle = play_1d_tile("sounds/terrain/stream.ogg", x, nearest_stream, true);
stream_sound_position = nearest_stream;
if (stream_sound_handle != -1) {
p.update_sound_positioning_values(stream_sound_handle, -1.0, to_audio_volume_step(MOUNTAIN_STREAM_VOLUME_STEP), true);
}
} else if (stream_sound_position != nearest_stream) {
update_sound_1d_tile(stream_sound_handle, nearest_stream);
stream_sound_position = nearest_stream;
}
}
}
void destroy() {
if (stream_sound_handle != -1) {
p.destroy_sound(stream_sound_handle);
stream_sound_handle = -1;
}
stream_sound_position = -1;
}
}
MountainRange@[] world_mountains;
MountainRange@ get_mountain_at(int world_x) {
for (uint i = 0; i < world_mountains.length(); i++) {
if (world_mountains[i].contains_position(world_x)) {
return @world_mountains[i];
}
}
return null;
}
int get_mountain_elevation_at(int world_x) {
MountainRange@ mountain = get_mountain_at(world_x);
if (mountain is null) return 0;
return mountain.get_elevation_at(world_x);
}
bool is_mountain_stream_at(int world_x) {
MountainRange@ mountain = get_mountain_at(world_x);
if (mountain is null) return false;
return mountain.is_stream_at(world_x);
}
void update_mountains() {
for (uint i = 0; i < world_mountains.length(); i++) {
world_mountains[i].update();
}
}
void clear_mountains() {
for (uint i = 0; i < world_mountains.length(); i++) {
world_mountains[i].destroy();
}
world_mountains.resize(0);
}