// World Objects // Small game types that can be caught in snares string[] small_game_types = {"rabbit", "squirrel", "raccoon", "opossum", "groundhog"}; int barricade_health = 0; bool barricade_initialized = false; int residents_count = 0; string[] zombie_sounds = {"sounds/enemies/zombie1.ogg"}; string[] bandit_sounds = {"sounds/enemies/bandit1.ogg", "sounds/enemies/bandit2.ogg"}; string[] goose_sounds = {"sounds/game/goose.ogg"}; class Zombie { int position; int health; string voice_sound; int sound_handle; timer move_timer; timer groan_timer; timer attack_timer; int next_groan_delay; Zombie(int pos) { position = pos; health = ZOMBIE_HEALTH; int sound_index = random(0, zombie_sounds.length() - 1); voice_sound = zombie_sounds[sound_index]; sound_handle = -1; move_timer.restart(); groan_timer.restart(); attack_timer.restart(); next_groan_delay = random(ZOMBIE_GROAN_MIN_DELAY, ZOMBIE_GROAN_MAX_DELAY); } } Zombie@[] zombies; class Bandit { int position; int health; string alert_sound; string weapon_type; // "spear" or "axe" int sound_handle; timer move_timer; timer alert_timer; timer attack_timer; int next_alert_delay; int move_interval; // Wandering behavior properties string behavior_state; // "aggressive" or "wandering" int wander_direction; // -1, 0, or 1 timer wander_direction_timer; int wander_direction_change_interval; Bandit(int pos, int expansion_start, int expansion_end) { // Spawn somewhere in the expanded area position = random(expansion_start, expansion_end); health = BANDIT_HEALTH; sound_handle = -1; // Choose random alert sound int sound_index = random(0, bandit_sounds.length() - 1); alert_sound = bandit_sounds[sound_index]; // Choose random weapon weapon_type = (random(0, 1) == 0) ? "spear" : "axe"; // Random movement speed within range move_interval = random(BANDIT_MOVE_INTERVAL_MIN, BANDIT_MOVE_INTERVAL_MAX); move_timer.restart(); alert_timer.restart(); attack_timer.restart(); next_alert_delay = random(BANDIT_ALERT_MIN_DELAY, BANDIT_ALERT_MAX_DELAY); // Initialize wandering behavior (start aggressive during invasion) behavior_state = "aggressive"; wander_direction = 0; wander_direction_change_interval = random(BANDIT_WANDER_DIRECTION_CHANGE_MIN, BANDIT_WANDER_DIRECTION_CHANGE_MAX); wander_direction_timer.restart(); } } Bandit@[] bandits; class FlyingCreatureConfig { string id; string drop_type; string[] sounds; string fall_sound; string impact_sound; int health; int move_interval_min; int move_interval_max; int min_height; int max_height; float sound_volume_step; int sound_delay_min; int sound_delay_max; int fall_speed; int fly_away_chance; int max_dist_from_water; int hourly_spawn_chance; int max_count; int sight_range; bool flee_on_sight; } FlyingCreatureConfig@[] flying_creature_configs; class FlyingCreature { int position; int health; int height; string state; // "flying", "falling" int area_start; int area_end; string creature_type; int sound_handle; int fall_sound_handle; timer move_timer; timer sound_timer; timer fall_timer; int next_move_delay; int next_sound_delay; string voice_sound; bool fading_out; bool ready_to_remove; timer fade_timer; FlyingCreature(string type, int pos, int home_start, int home_end, FlyingCreatureConfig@ cfg) { position = pos; health = cfg.health; height = random(cfg.min_height, cfg.max_height); state = "flying"; area_start = home_start; area_end = home_end; creature_type = type; sound_handle = -1; fall_sound_handle = -1; if (cfg.sounds.length() > 0) { voice_sound = cfg.sounds[random(0, cfg.sounds.length() - 1)]; } move_timer.restart(); sound_timer.restart(); next_move_delay = random(cfg.move_interval_min, cfg.move_interval_max); next_sound_delay = random(cfg.sound_delay_min, cfg.sound_delay_max); fading_out = false; ready_to_remove = false; } } FlyingCreature@[] flying_creatures; string get_random_small_game() { int index = random(0, small_game_types.length() - 1); return small_game_types[index]; } class WorldDrop { int position; string type; int sound_handle; WorldDrop(int pos, string t) { position = pos; type = t; sound_handle = -1; // Start looping item sound at position sound_handle = p.play_1d("sounds/items/item.ogg", x, position, true); if (sound_handle != -1) { p.update_sound_positioning_values(sound_handle, -1.0, 3.0, true); } } void update() { if (sound_handle == -1 || !p.sound_is_active(sound_handle)) { sound_handle = p.play_1d("sounds/items/item.ogg", x, position, true); if (sound_handle != -1) { p.update_sound_positioning_values(sound_handle, -1.0, 3.0, true); } } else { // Update source position for 1d sound p.update_sound_1d(sound_handle, position); } } void destroy() { if (sound_handle != -1) { p.destroy_sound(sound_handle); sound_handle = -1; } } } WorldDrop@[] world_drops; void add_world_drop(int pos, string type) { WorldDrop@ d = WorldDrop(pos, type); world_drops.insert_last(d); } void update_world_drops() { for (uint i = 0; i < world_drops.length(); i++) { world_drops[i].update(); } } WorldDrop@ get_drop_at(int pos) { for (uint i = 0; i < world_drops.length(); i++) { if (world_drops[i].position == pos) { return @world_drops[i]; } } return null; } void remove_drop_at(int pos) { for (uint i = 0; i < world_drops.length(); i++) { if (world_drops[i].position == pos) { world_drops[i].destroy(); world_drops.remove_at(i); return; } } } void clear_world_drops() { for (uint i = 0; i < world_drops.length(); i++) { world_drops[i].destroy(); } world_drops.resize(0); } bool try_pickup_small_game(string game_type) { if (inv_small_game >= get_personal_stack_limit()) { screen_reader_speak("You can't carry any more small game.", true); return false; } inv_small_game++; inv_small_game_types.insert_last(game_type); screen_reader_speak("Picked up " + game_type + ".", true); return true; } bool try_pickup_world_drop(WorldDrop@ drop) { if (get_flying_creature_config_by_drop_type(drop.type) !is null) { return try_pickup_small_game(drop.type); } screen_reader_speak("Picked up " + drop.type + ".", true); return true; } class WorldSnare { int position; bool has_catch; string catch_type; // What type of small game was caught int catch_chance; int escape_chance; int sound_handle; timer minute_timer; bool active; // To prevent immediate breakage on placement WorldSnare(int pos) { position = pos; has_catch = false; catch_type = ""; catch_chance = 5; escape_chance = 5; active = false; // Becomes active when player steps off sound_handle = -1; minute_timer.restart(); } void update() { // Activate if player moves away if (!active && x != position) { active = true; minute_timer.restart(); } int snare_distance = x - position; if (snare_distance < 0) snare_distance = -snare_distance; if (snare_distance <= SNARE_SOUND_RANGE) { if (sound_handle == -1 || !p.sound_is_active(sound_handle)) { sound_handle = p.play_1d("sounds/actions/set_snare.ogg", x, position, true); if (sound_handle != -1) { p.update_sound_positioning_values(sound_handle, SNARE_SOUND_PAN_STEP, SNARE_SOUND_VOLUME_STEP, true); } } } else if (sound_handle != -1) { p.destroy_sound(sound_handle); sound_handle = -1; } // Every minute logic (only when active) if (active && minute_timer.elapsed >= 60000) { minute_timer.restart(); if (has_catch) { // Animal trying to escape if (escape_chance < 95) escape_chance += 5; int roll = random(1, 100); if (roll <= escape_chance) { // Animal escaped! notify("A " + catch_type + " escaped from your snare at x " + position + " y 0!"); remove_snare_at(position); return; } } else { // Trying to catch small game if (catch_chance < 75) catch_chance += 5; int roll = random(1, 100); if (roll <= catch_chance) { // Caught something! has_catch = true; catch_type = get_random_small_game(); escape_chance = 5; // Reset escape chance notify(catch_type + " caught in snare at x " + position + " y 0!"); } } } } void destroy() { if (sound_handle != -1) { p.destroy_sound(sound_handle); sound_handle = -1; } } } WorldSnare@[] world_snares; class WorldFire { int position; int sound_handle; timer fuel_timer; int fuel_remaining; bool low_fuel_warned; WorldFire(int pos) { position = pos; sound_handle = -1; fuel_remaining = 720000; // Start with 12 minutes (12 hours in-game) low_fuel_warned = false; fuel_timer.restart(); } void add_fuel(int amount) { fuel_remaining += amount; low_fuel_warned = false; } bool is_burning() { return fuel_remaining > 0; } void update() { // Update fuel if (fuel_remaining > 0) { int elapsed = fuel_timer.elapsed; fuel_timer.restart(); fuel_remaining -= elapsed; // 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 x " + position + " y " + y + " is getting low!"); } // Fire went out if (fuel_remaining <= 0) { fuel_remaining = 0; notify("Fire at x " + position + " y " + y + " has gone out."); if (sound_handle != -1) { p.destroy_sound(sound_handle); sound_handle = -1; } return; } } // Hard cutoff for fire sound. if (is_burning()) { int fire_distance = x - position; if (fire_distance < 0) fire_distance = -fire_distance; if (fire_distance <= FIRE_SOUND_RANGE) { if (sound_handle == -1 || !p.sound_is_active(sound_handle)) { sound_handle = p.play_1d("sounds/items/fire.ogg", x, position, true); if (sound_handle != -1) { p.update_sound_positioning_values(sound_handle, -1.0, FIRE_SOUND_VOLUME_STEP, true); } } } else if (sound_handle != -1) { p.destroy_sound(sound_handle); sound_handle = -1; } } } void destroy() { if (sound_handle != -1) { p.destroy_sound(sound_handle); sound_handle = -1; } } } WorldFire@[] world_fires; class WorldFirepit { int position; WorldFirepit(int pos) { position = pos; } } WorldFirepit@[] world_firepits; class WorldHerbGarden { int position; WorldHerbGarden(int pos) { position = pos; } } WorldHerbGarden@[] world_herb_gardens; class WorldStorage { int position; WorldStorage(int pos) { position = pos; } } WorldStorage@[] world_storages; class WorldPasture { int position; WorldPasture(int pos) { position = pos; } } WorldPasture@[] world_pastures; class WorldStable { int position; WorldStable(int pos) { position = pos; } } WorldStable@[] world_stables; class WorldAltar { int position; WorldAltar(int pos) { position = pos; } } WorldAltar@[] world_altars; 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) { return pos >= start_position && pos <= end_position; } int get_width() { return end_position - start_position + 1; } int get_center_position() { return (start_position + end_position) / 2; } void update() { 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 = p.play_1d("sounds/terrain/stream.ogg", x, sound_pos, true); sound_position = sound_pos; if (sound_handle != -1) { p.update_sound_positioning_values(sound_handle, -1.0, STREAM_SOUND_VOLUME_STEP, true); } } else if (sound_position != sound_pos) { p.update_sound_1d(sound_handle, sound_pos); sound_position = sound_pos; } } void destroy() { if (sound_handle != -1) { p.destroy_sound(sound_handle); sound_handle = -1; } sound_position = -1; } } WorldStream@[] world_streams; void add_world_snare(int pos) { WorldSnare@ s = WorldSnare(pos); world_snares.insert_last(s); } void add_world_fire(int pos) { WorldFire@ f = WorldFire(pos); world_fires.insert_last(f); } void add_world_firepit(int pos) { WorldFirepit@ fp = WorldFirepit(pos); world_firepits.insert_last(fp); } void add_world_storage(int pos) { WorldStorage@ s = WorldStorage(pos); world_storages.insert_last(s); } void add_world_pasture(int pos) { WorldPasture@ p = WorldPasture(pos); world_pastures.insert_last(p); } void add_world_stable(int pos) { WorldStable@ s = WorldStable(pos); world_stables.insert_last(s); } void add_world_altar(int pos) { WorldAltar@ a = WorldAltar(pos); world_altars.insert_last(a); } WorldStorage@ get_storage_at(int pos) { for (uint i = 0; i < world_storages.length(); i++) { if (world_storages[i].position == pos) { return @world_storages[i]; } } return null; } WorldPasture@ get_pasture_at(int pos) { for (uint i = 0; i < world_pastures.length(); i++) { if (world_pastures[i].position == pos) { return @world_pastures[i]; } } return null; } WorldStable@ get_stable_at(int pos) { for (uint i = 0; i < world_stables.length(); i++) { if (world_stables[i].position == pos) { return @world_stables[i]; } } return null; } WorldAltar@ get_altar_at(int pos) { for (uint i = 0; i < world_altars.length(); i++) { if (world_altars[i].position == pos) { return @world_altars[i]; } } return null; } void update_world_objects() { for (uint i = 0; i < world_snares.length(); i++) { world_snares[i].update(); } for (uint i = 0; i < world_fires.length(); i++) { world_fires[i].update(); } } WorldSnare@ get_snare_at(int pos) { for (uint i = 0; i < world_snares.length(); i++) { if (world_snares[i].position == pos) { return @world_snares[i]; } } return null; } void remove_snare_at(int pos) { for (uint i = 0; i < world_snares.length(); i++) { if (world_snares[i].position == pos) { world_snares[i].destroy(); world_snares.remove_at(i); return; } } } // Called when player moves onto a tile void check_snare_collision(int player_x) { WorldSnare@ s = get_snare_at(player_x); if (s != null && s.active) { // Break the snare p.play_stationary("sounds/actions/break_snare.ogg", false); if (s.has_catch) { screen_reader_speak("You stepped on your snare! The " + s.catch_type + " escaped.", true); } else { screen_reader_speak("You stepped on your snare and broke it!", true); } remove_snare_at(player_x); } } void update_snares() { for (int i = int(world_snares.length()) - 1; i >= 0; i--) { world_snares[i].update(); } } void update_streams() { for (uint i = 0; i < world_streams.length(); i++) { world_streams[i].update(); } } void update_fires() { // Update all fires and remove any that have burned out for (uint i = 0; i < world_fires.length(); i++) { world_fires[i].update(); } // Remove dead fires for (uint i = 0; i < world_fires.length(); i++) { if (!world_fires[i].is_burning()) { world_fires[i].destroy(); world_fires.remove_at(i); i--; } } } WorldFire@ get_fire_at(int pos) { for (uint i = 0; i < world_fires.length(); i++) { if (world_fires[i].position == pos) { return @world_fires[i]; } } return null; } WorldFire@ get_fire_near(int pos) { // Check for fire at current position or adjacent tiles for (int check_x = pos - 1; check_x <= pos + 1; check_x++) { WorldFire@ fire = get_fire_at(check_x); if (fire != null && fire.is_burning()) { return @fire; } } return null; } WorldFire@ get_fire_within_range(int pos, int range) { // Check for fire within specified range for (int check_x = pos - range; check_x <= pos + range; check_x++) { WorldFire@ fire = get_fire_at(check_x); if (fire != null && fire.is_burning()) { return @fire; } } return null; } WorldFirepit@ get_firepit_at(int pos) { for (uint i = 0; i < world_firepits.length(); i++) { if (world_firepits[i].position == pos) { return @world_firepits[i]; } } return null; } WorldFirepit@ get_firepit_near(int pos, int range) { // Check for firepit within specified range for (int check_x = pos - range; check_x <= pos + range; check_x++) { WorldFirepit@ firepit = get_firepit_at(check_x); if (firepit != null) { return @firepit; } } return null; } void add_world_herb_garden(int pos) { WorldHerbGarden@ hg = WorldHerbGarden(pos); world_herb_gardens.insert_last(hg); } void init_barricade() { if (barricade_initialized) { return; } barricade_health = BARRICADE_BASE_HEALTH; barricade_initialized = true; } int add_barricade_health(int amount) { if (amount <= 0) { return 0; } int before = barricade_health; barricade_health += amount; if (barricade_health > BARRICADE_MAX_HEALTH) { barricade_health = BARRICADE_MAX_HEALTH; } return barricade_health - before; } void clear_zombies() { if (zombies.length() == 0) return; for (uint i = 0; i < zombies.length(); i++) { if (zombies[i].sound_handle != -1) { p.destroy_sound(zombies[i].sound_handle); zombies[i].sound_handle = -1; } } zombies.resize(0); } Zombie@ get_zombie_at(int pos) { for (uint i = 0; i < zombies.length(); i++) { if (zombies[i].position == pos) { return @zombies[i]; } } return null; } void spawn_zombie() { int spawn_x = -1; for (int attempts = 0; attempts < 20; attempts++) { int candidate = random(BASE_END + 1, MAP_SIZE - 1); if (get_zombie_at(candidate) == null) { spawn_x = candidate; break; } } if (spawn_x == -1) { spawn_x = random(BASE_END + 1, MAP_SIZE - 1); } Zombie@ z = Zombie(spawn_x); zombies.insert_last(z); z.sound_handle = play_creature_voice(z.voice_sound, x, spawn_x, ZOMBIE_SOUND_VOLUME_STEP); } void try_attack_barricade(Zombie@ zombie) { if (barricade_health <= 0) return; if (zombie.attack_timer.elapsed < ZOMBIE_ATTACK_INTERVAL) return; zombie.attack_timer.restart(); int damage = random(ZOMBIE_DAMAGE_MIN, ZOMBIE_DAMAGE_MAX); barricade_health -= damage; if (barricade_health < 0) barricade_health = 0; play_creature_attack_sound("sounds/enemies/zombie_hits_player.ogg", x, zombie.position, ZOMBIE_SOUND_VOLUME_STEP); // Resident defense counter-attack if (can_residents_defend()) { int counterDamage = perform_resident_defense(); if (counterDamage > 0) { zombie.health -= counterDamage; if (zombie.health <= 0 && x <= BASE_END) { screen_reader_speak("Residents killed an attacking zombie.", true); } } } if (barricade_health == 0) { notify("The barricade has fallen!"); } } bool can_zombie_attack_player(Zombie@ zombie) { if (player_health <= 0) { return false; } if (barricade_health > 0 && x <= BASE_END) { return false; } if (abs(zombie.position - x) > 1) { return false; } return y <= ZOMBIE_ATTACK_MAX_HEIGHT; } bool try_attack_player(Zombie@ zombie) { if (!can_zombie_attack_player(zombie)) { return false; } if (zombie.attack_timer.elapsed < ZOMBIE_ATTACK_INTERVAL) { return false; } zombie.attack_timer.restart(); int damage = random(ZOMBIE_DAMAGE_MIN, ZOMBIE_DAMAGE_MAX); player_health -= damage; if (player_health < 0) { player_health = 0; } play_creature_attack_sound("sounds/enemies/zombie_hits_player.ogg", x, zombie.position, ZOMBIE_SOUND_VOLUME_STEP); return true; } void update_zombie(Zombie@ zombie) { if (zombie.groan_timer.elapsed > zombie.next_groan_delay) { zombie.groan_timer.restart(); zombie.next_groan_delay = random(ZOMBIE_GROAN_MIN_DELAY, ZOMBIE_GROAN_MAX_DELAY); zombie.sound_handle = play_creature_voice(zombie.voice_sound, x, zombie.position, ZOMBIE_SOUND_VOLUME_STEP); } if (try_attack_player(zombie)) { return; } if (zombie.move_timer.elapsed < ZOMBIE_MOVE_INTERVAL) return; zombie.move_timer.restart(); if (barricade_health > 0 && zombie.position == BASE_END + 1) { try_attack_barricade(zombie); return; } int direction = 0; if (x > BASE_END) { if (x > zombie.position) { direction = 1; } else if (x < zombie.position) { direction = -1; } else { return; } } else { direction = random(-1, 1); if (direction == 0) return; } int target_x = zombie.position + direction; if (target_x < 0 || target_x >= MAP_SIZE) return; if (target_x <= BASE_END && barricade_health > 0) { try_attack_barricade(zombie); return; } zombie.position = target_x; play_creature_footstep(x, zombie.position, BASE_END, GRASS_END, ZOMBIE_FOOTSTEP_MAX_DISTANCE, ZOMBIE_SOUND_VOLUME_STEP); } void update_zombies() { if (is_daytime) { clear_zombies(); return; } while (zombies.length() < ZOMBIE_MAX_COUNT) { spawn_zombie(); } for (uint i = 0; i < zombies.length(); i++) { update_zombie(zombies[i]); } } bool damage_zombie_at(int pos, int damage) { for (uint i = 0; i < zombies.length(); i++) { if (zombies[i].position == pos) { zombies[i].health -= damage; if (zombies[i].health <= 0) { if (zombies[i].sound_handle != -1) { p.destroy_sound(zombies[i].sound_handle); zombies[i].sound_handle = -1; } play_creature_death_sound("sounds/enemies/enemy_falls.ogg", x, pos, ZOMBIE_SOUND_VOLUME_STEP); zombies.remove_at(i); } return true; } } return false; } WorldHerbGarden@ get_herb_garden_at(int pos) { for (uint i = 0; i < world_herb_gardens.length(); i++) { if (world_herb_gardens[i].position == pos) { return @world_herb_gardens[i]; } } return null; } WorldHerbGarden@ get_herb_garden_at_base() { // Check if herb garden exists anywhere in base area (0-4) for (uint i = 0; i < world_herb_gardens.length(); i++) { if (world_herb_gardens[i].position <= BASE_END) { return @world_herb_gardens[i]; } } return null; } // Bandit Functions void clear_bandits() { if (bandits.length() == 0) return; bandits.resize(0); } Bandit@ get_bandit_at(int pos) { for (uint i = 0; i < bandits.length(); i++) { if (bandits[i].position == pos) { return @bandits[i]; } } return null; } void spawn_bandit(int expansion_start, int expansion_end) { int spawn_x = -1; for (int attempts = 0; attempts < 20; attempts++) { int candidate = random(expansion_start, expansion_end); if (get_bandit_at(candidate) == null) { spawn_x = candidate; break; } } if (spawn_x == -1) { spawn_x = random(expansion_start, expansion_end); } Bandit@ b = Bandit(spawn_x, expansion_start, expansion_end); bandits.insert_last(b); b.sound_handle = play_creature_voice(b.alert_sound, x, spawn_x, BANDIT_SOUND_VOLUME_STEP); } bool can_bandit_attack_player(Bandit@ bandit) { if (player_health <= 0) { return false; } // Cannot attack player if barricade is up and player is in base if (barricade_health > 0 && x <= BASE_END) { return false; } if (abs(bandit.position - x) > 1) { return false; } return y <= BANDIT_ATTACK_MAX_HEIGHT; } bool try_attack_player_bandit(Bandit@ bandit) { if (!can_bandit_attack_player(bandit)) { return false; } if (bandit.attack_timer.elapsed < BANDIT_ATTACK_INTERVAL) { return false; } bandit.attack_timer.restart(); // Play weapon swing sound based on bandit's weapon if (bandit.weapon_type == "spear") { play_creature_attack_sound("sounds/weapons/spear_swing.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP); } else if (bandit.weapon_type == "axe") { play_creature_attack_sound("sounds/weapons/axe_swing.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP); } int damage = random(BANDIT_DAMAGE_MIN, BANDIT_DAMAGE_MAX); player_health -= damage; if (player_health < 0) { player_health = 0; } // Play hit sound if (bandit.weapon_type == "spear") { play_creature_attack_sound("sounds/weapons/spear_hit.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP); } else if (bandit.weapon_type == "axe") { play_creature_attack_sound("sounds/weapons/axe_hit.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP); } return true; } void try_attack_barricade_bandit(Bandit@ bandit) { if (barricade_health <= 0) return; if (bandit.attack_timer.elapsed < BANDIT_ATTACK_INTERVAL) return; bandit.attack_timer.restart(); // Bandits do 1-2 damage to barricade int damage = random(BANDIT_DAMAGE_MIN, BANDIT_DAMAGE_MAX); barricade_health -= damage; if (barricade_health < 0) barricade_health = 0; // Play weapon swing sound if (bandit.weapon_type == "spear") { play_creature_attack_sound("sounds/weapons/spear_swing.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP); play_creature_attack_sound("sounds/weapons/spear_hit.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP); } else if (bandit.weapon_type == "axe") { play_creature_attack_sound("sounds/weapons/axe_swing.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP); play_creature_attack_sound("sounds/weapons/axe_hit.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP); } // Resident defense counter-attack if (can_residents_defend()) { int counterDamage = perform_resident_defense(); if (counterDamage > 0) { bandit.health -= counterDamage; if (bandit.health <= 0 && x <= BASE_END) { screen_reader_speak("Residents killed an attacking bandit.", true); } } } if (barricade_health == 0) { notify("The barricade has fallen!"); } } void update_bandit(Bandit@ bandit) { // Play alert sound at intervals if (bandit.alert_timer.elapsed > bandit.next_alert_delay) { bandit.alert_timer.restart(); bandit.next_alert_delay = random(BANDIT_ALERT_MIN_DELAY, BANDIT_ALERT_MAX_DELAY); bandit.sound_handle = play_creature_voice(bandit.alert_sound, x, bandit.position, BANDIT_SOUND_VOLUME_STEP); } if (try_attack_player_bandit(bandit)) { return; } if (bandit.move_timer.elapsed < bandit.move_interval) return; bandit.move_timer.restart(); // If barricade is up and bandit is at the edge of base, attack barricade if (barricade_health > 0 && bandit.position == BASE_END + 1) { try_attack_barricade_bandit(bandit); return; } // State-based behavior if (bandit.behavior_state == "wandering") { // Check if player is within detection radius int distance = abs(bandit.position - x); if (distance <= BANDIT_DETECTION_RADIUS) { // Player detected! Switch to aggressive bandit.behavior_state = "aggressive"; } else { // Continue wandering if (bandit.wander_direction_timer.elapsed > bandit.wander_direction_change_interval) { // Time to change direction bandit.wander_direction = random(-1, 1); bandit.wander_direction_change_interval = random(BANDIT_WANDER_DIRECTION_CHANGE_MIN, BANDIT_WANDER_DIRECTION_CHANGE_MAX); bandit.wander_direction_timer.restart(); } // Move in wander direction (if not 0) if (bandit.wander_direction != 0) { int target_x = bandit.position + bandit.wander_direction; // Check bounds if (target_x >= 0 && target_x < MAP_SIZE) { // Don't wander into base if barricade is up if (target_x <= BASE_END && barricade_health > 0) { // Change direction instead bandit.wander_direction = -bandit.wander_direction; } else { bandit.position = target_x; play_creature_footstep(x, bandit.position, BASE_END, GRASS_END, BANDIT_FOOTSTEP_MAX_DISTANCE, BANDIT_SOUND_VOLUME_STEP); } } else { // Hit map boundary, reverse direction bandit.wander_direction = -bandit.wander_direction; } } return; } } // Aggressive behavior (original logic) if (bandit.behavior_state == "aggressive") { // Move toward player int direction = 0; if (x > BASE_END) { // Player is outside base, move toward them if (x > bandit.position) { direction = 1; } else if (x < bandit.position) { direction = -1; } else { return; } } else { // Player is in base, move toward base edge if (bandit.position > BASE_END + 1) { direction = -1; } else { return; // Already at base edge } } int target_x = bandit.position + direction; if (target_x < 0 || target_x >= MAP_SIZE) return; // Don't enter base if barricade is up if (target_x <= BASE_END && barricade_health > 0) { try_attack_barricade_bandit(bandit); return; } bandit.position = target_x; play_creature_footstep(x, bandit.position, BASE_END, GRASS_END, BANDIT_FOOTSTEP_MAX_DISTANCE, BANDIT_SOUND_VOLUME_STEP); } } void update_bandits() { for (uint i = 0; i < bandits.length(); i++) { update_bandit(bandits[i]); } } bool damage_bandit_at(int pos, int damage) { for (uint i = 0; i < bandits.length(); i++) { if (bandits[i].position == pos) { bandits[i].health -= damage; if (bandits[i].health <= 0) { if (bandits[i].sound_handle != -1) { p.destroy_sound(bandits[i].sound_handle); bandits[i].sound_handle = -1; } play_creature_death_sound("sounds/enemies/enemy_falls.ogg", x, pos, BANDIT_SOUND_VOLUME_STEP); bandits.remove_at(i); } else { } return true; } } return false; } // Stream Functions void add_world_stream(int start_pos, int width) { WorldStream@ s = WorldStream(start_pos, width); world_streams.insert_last(s); } WorldStream@ get_stream_at(int pos) { for (uint i = 0; i < world_streams.length(); i++) { if (world_streams[i].contains_position(pos)) { return @world_streams[i]; } } return null; } 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 = p.play_1d("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, MOUNTAIN_STREAM_VOLUME_STEP, true); } } else if (stream_sound_position != nearest_stream) { p.update_sound_1d(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); } // Flying Creature Functions void init_flying_creature_configs() { flying_creature_configs.resize(0); FlyingCreatureConfig@ goose_cfg = FlyingCreatureConfig(); goose_cfg.id = "goose"; goose_cfg.drop_type = "goose"; goose_cfg.sounds = goose_sounds; goose_cfg.fall_sound = "sounds/actions/falling.ogg"; goose_cfg.impact_sound = "sounds/game/game_falls.ogg"; goose_cfg.health = GOOSE_HEALTH; goose_cfg.move_interval_min = GOOSE_MOVE_INTERVAL_MIN; goose_cfg.move_interval_max = GOOSE_MOVE_INTERVAL_MAX; goose_cfg.min_height = GOOSE_FLYING_HEIGHT_MIN; goose_cfg.max_height = GOOSE_FLYING_HEIGHT_MAX; goose_cfg.sound_volume_step = GOOSE_SOUND_VOLUME_STEP; goose_cfg.sound_delay_min = GOOSE_FLIGHT_SOUND_DELAY_MIN; goose_cfg.sound_delay_max = GOOSE_FLIGHT_SOUND_DELAY_MAX; goose_cfg.fall_speed = GOOSE_FALL_SPEED; goose_cfg.fly_away_chance = GOOSE_FLY_AWAY_CHANCE; goose_cfg.max_dist_from_water = GOOSE_MAX_DIST_FROM_WATER; goose_cfg.hourly_spawn_chance = GOOSE_HOURLY_SPAWN_CHANCE; goose_cfg.max_count = GOOSE_MAX_COUNT; goose_cfg.sight_range = GOOSE_SIGHT_RANGE; goose_cfg.flee_on_sight = false; flying_creature_configs.insert_last(goose_cfg); } FlyingCreatureConfig@ get_flying_creature_config(string creature_type) { for (uint i = 0; i < flying_creature_configs.length(); i++) { if (flying_creature_configs[i].id == creature_type) { return @flying_creature_configs[i]; } } return null; } FlyingCreatureConfig@ get_flying_creature_config_by_drop_type(string drop_type) { for (uint i = 0; i < flying_creature_configs.length(); i++) { if (flying_creature_configs[i].drop_type == drop_type) { return @flying_creature_configs[i]; } } return null; } void clear_flying_creatures() { for (uint i = 0; i < flying_creatures.length(); i++) { if (flying_creatures[i].sound_handle != -1) { p.destroy_sound(flying_creatures[i].sound_handle); flying_creatures[i].sound_handle = -1; } if (flying_creatures[i].fall_sound_handle != -1) { p.destroy_sound(flying_creatures[i].fall_sound_handle); flying_creatures[i].fall_sound_handle = -1; } } flying_creatures.resize(0); } FlyingCreature@ get_flying_creature_at(int pos) { for (uint i = 0; i < flying_creatures.length(); i++) { if (flying_creatures[i].position == pos) { return @flying_creatures[i]; } } return null; } int get_flying_creature_count(string creature_type) { int count = 0; for (uint i = 0; i < flying_creatures.length(); i++) { if (flying_creatures[i].creature_type == creature_type) { count++; } } return count; } bool get_random_flying_creature_area(FlyingCreatureConfig@ cfg, int &out area_start, int &out area_end) { int stream_count = int(world_streams.length()); int mountain_stream_count = 0; for (uint i = 0; i < world_mountains.length(); i++) { mountain_stream_count += int(world_mountains[i].stream_positions.length()); } int total_areas = stream_count + mountain_stream_count; if (total_areas <= 0) return false; int pick = random(0, total_areas - 1); if (pick < stream_count) { area_start = world_streams[pick].start_position; area_end = world_streams[pick].end_position; } else { pick -= stream_count; for (uint i = 0; i < world_mountains.length(); i++) { int local_count = int(world_mountains[i].stream_positions.length()); if (pick < local_count) { int stream_pos = world_mountains[i].start_position + world_mountains[i].stream_positions[pick]; area_start = stream_pos; area_end = stream_pos; break; } pick -= local_count; } } area_start -= cfg.max_dist_from_water; area_end += cfg.max_dist_from_water; if (area_start < 0) area_start = 0; if (area_end >= MAP_SIZE) area_end = MAP_SIZE - 1; return true; } bool find_flying_creature_spawn(FlyingCreatureConfig@ cfg, int &out spawn_x, int &out area_start, int &out area_end) { if (!get_random_flying_creature_area(cfg, area_start, area_end)) return false; for (int attempts = 0; attempts < 20; attempts++) { int candidate = random(area_start, area_end); if (get_flying_creature_at(candidate) == null) { spawn_x = candidate; return true; } } return false; } void fly_away_flying_creature(FlyingCreature@ creature, FlyingCreatureConfig@ cfg) { creature.state = "fading"; creature.fading_out = true; creature.ready_to_remove = false; creature.fade_timer.restart(); creature.health = 0; if (creature.sound_handle == -1 || !p.sound_is_active(creature.sound_handle)) { creature.ready_to_remove = true; } if (creature.fall_sound_handle != -1) { p.destroy_sound(creature.fall_sound_handle); creature.fall_sound_handle = -1; } } bool spawn_flying_creature(string creature_type) { FlyingCreatureConfig@ cfg = get_flying_creature_config(creature_type); if (cfg is null) return false; int spawn_x = -1; int area_start = 0; int area_end = 0; if (!find_flying_creature_spawn(cfg, spawn_x, area_start, area_end)) { return false; } FlyingCreature@ c = FlyingCreature(creature_type, spawn_x, area_start, area_end, cfg); flying_creatures.insert_last(c); c.sound_handle = play_creature_voice(c.voice_sound, x, spawn_x, cfg.sound_volume_step); return true; } void update_flying_creature(FlyingCreature@ creature) { FlyingCreatureConfig@ cfg = get_flying_creature_config(creature.creature_type); if (cfg is null) return; if (creature.state == "fading") { if (!creature.fading_out) { creature.fading_out = true; creature.fade_timer.restart(); } if (creature.sound_handle != -1 && p.sound_is_active(creature.sound_handle)) { float progress = float(creature.fade_timer.elapsed) / float(FLYING_CREATURE_FADE_OUT_DURATION); if (progress < 0.0) progress = 0.0; if (progress > 1.0) progress = 1.0; float volume = 0.0 + (FLYING_CREATURE_FADE_OUT_MIN_VOLUME * progress); p.update_sound_start_values(creature.sound_handle, 0.0, volume, 1.0); } if (creature.fade_timer.elapsed >= FLYING_CREATURE_FADE_OUT_DURATION) { if (creature.sound_handle != -1) { p.destroy_sound(creature.sound_handle); creature.sound_handle = -1; } creature.ready_to_remove = true; } return; } if (creature.state == "flying") { if (creature.position < creature.area_start || creature.position > creature.area_end) { fly_away_flying_creature(creature, cfg); return; } if (creature.sound_timer.elapsed > creature.next_sound_delay) { creature.sound_timer.restart(); creature.next_sound_delay = random(cfg.sound_delay_min, cfg.sound_delay_max); creature.sound_handle = play_creature_voice(creature.voice_sound, x, creature.position, cfg.sound_volume_step); } if (cfg.fly_away_chance > 0 && random(1, 1000) <= cfg.fly_away_chance) { fly_away_flying_creature(creature, cfg); return; } if (creature.move_timer.elapsed > creature.next_move_delay) { creature.move_timer.restart(); creature.next_move_delay = random(cfg.move_interval_min, cfg.move_interval_max); int dir = 0; if (cfg.flee_on_sight && cfg.sight_range > 0) { int distance_to_player = abs(x - creature.position); if (distance_to_player <= cfg.sight_range) { if (x > creature.position) dir = -1; else if (x < creature.position) dir = 1; } } if (dir == 0) dir = random(-1, 1); if (dir != 0) { int target_x = creature.position + dir; if (target_x < creature.area_start || target_x > creature.area_end) { fly_away_flying_creature(creature, cfg); return; } if (target_x >= 0 && target_x < MAP_SIZE) { creature.position = target_x; if (creature.sound_handle != -1 && p.sound_is_active(creature.sound_handle)) { p.update_sound_1d(creature.sound_handle, creature.position); } } } } } else if (creature.state == "falling") { if (creature.fall_timer.elapsed > cfg.fall_speed) { creature.fall_timer.restart(); creature.height--; if (creature.fall_sound_handle != -1) { p.destroy_sound(creature.fall_sound_handle); } float pitch_percent = 50.0 + (50.0 * (float(creature.height) / float(cfg.max_height))); if (pitch_percent < 50.0) pitch_percent = 50.0; if (pitch_percent > 100.0) pitch_percent = 100.0; creature.fall_sound_handle = p.play_extended_1d(cfg.fall_sound, x, creature.position, 0, 0, true, 0, 0.0, 0.0, pitch_percent); if (creature.fall_sound_handle != -1) { p.update_sound_positioning_values(creature.fall_sound_handle, -1.0, cfg.sound_volume_step, true); } if (creature.height <= 0) { if (creature.fall_sound_handle != -1) { p.destroy_sound(creature.fall_sound_handle); creature.fall_sound_handle = -1; } play_creature_death_sound(cfg.impact_sound, x, creature.position, cfg.sound_volume_step); notify("A " + creature.creature_type + " fell from the sky at " + creature.position + "!"); add_world_drop(creature.position, cfg.drop_type); creature.health = 0; } } } } void update_flying_creatures() { for (uint i = 0; i < flying_creatures.length(); i++) { update_flying_creature(flying_creatures[i]); if (flying_creatures[i].health <= 0) { if (flying_creatures[i].state == "falling" && flying_creatures[i].height <= 0) { flying_creatures.remove_at(i); i--; } else if (flying_creatures[i].state == "flying") { flying_creatures.remove_at(i); i--; } else if (flying_creatures[i].state == "fading" && flying_creatures[i].ready_to_remove) { flying_creatures.remove_at(i); i--; } } } } void attempt_hourly_flying_creature_spawn() { for (uint i = 0; i < flying_creature_configs.length(); i++) { FlyingCreatureConfig@ cfg = flying_creature_configs[i]; if (get_flying_creature_count(cfg.id) >= cfg.max_count) continue; if (random(1, 100) <= cfg.hourly_spawn_chance) { spawn_flying_creature(cfg.id); } } } bool damage_flying_creature_at(int pos, int damage) { for (uint i = 0; i < flying_creatures.length(); i++) { if (flying_creatures[i].position == pos && flying_creatures[i].state == "flying") { FlyingCreatureConfig@ cfg = get_flying_creature_config(flying_creatures[i].creature_type); if (cfg is null) return false; flying_creatures[i].health -= damage; if (flying_creatures[i].health <= 0) { flying_creatures[i].state = "falling"; flying_creatures[i].fall_timer.restart(); if (flying_creatures[i].sound_handle != -1) { p.destroy_sound(flying_creatures[i].sound_handle); flying_creatures[i].sound_handle = -1; } float pitch_percent = 50.0 + (50.0 * (float(flying_creatures[i].height) / float(cfg.max_height))); flying_creatures[i].fall_sound_handle = p.play_extended_1d(cfg.fall_sound, x, pos, 0, 0, true, 0, 0.0, 0.0, pitch_percent); if (flying_creatures[i].fall_sound_handle != -1) { p.update_sound_positioning_values(flying_creatures[i].fall_sound_handle, -1.0, cfg.sound_volume_step, true); } } return true; } } return false; }