1752 lines
53 KiB
Plaintext
1752 lines
53 KiB
Plaintext
// 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;
|
|
}
|