Files
draugnorak/src/time_system.nvgt

1046 lines
34 KiB
Plaintext

// Time System
// 1 real minute = 1 in-game hour
const int MS_PER_HOUR = 60000;
int current_hour = 8; // Start at 8 AM
int current_day = 1; // Track current day
timer hour_timer;
int day_sound_handle = -1;
int night_sound_handle = -1;
bool is_daytime = true;
bool sun_setting_warned = false;
bool sunrise_warned = false;
float playerItemBreakChance = PLAYER_ITEM_BREAK_CHANCE_MIN;
int playerItemBreaksToday = 0;
bool playerItemBreakPending = false;
int playerItemBreakPendingType = -1;
string playerItemBreakMessage = "";
int playerItemBreakSoundHandle = -1;
bool playerItemBreakSoundPending = false;
// Crossfade state
bool crossfade_active = false;
bool crossfade_to_night = false; // true = fading to night, false = fading to day
timer crossfade_timer;
const int CROSSFADE_DURATION = 60000; // 1 minute (1 game hour)
const float CROSSFADE_MIN_VOLUME = -25.0; // dB, keep overlap audible during crossfade
const float CROSSFADE_MAX_VOLUME = 0.0; // dB, full volume
// Expansion and invasion tracking
bool area_expanded_today = false;
bool invasion_active = false;
int invasion_start_hour = -1;
string[] expanded_terrain_types;
int invasion_chance = 25;
bool invasion_triggered_today = false;
bool invasion_roll_done_today = false;
int invasion_scheduled_hour = -1;
string invasion_enemy_type = "bandit";
bool invasion_started_once = false;
// Zombie swarm tracking
bool zombie_swarm_active = false;
int zombie_swarm_start_hour = -1;
int zombie_swarm_scheduled_hour = -1;
bool zombie_swarm_triggered_today = false;
bool zombie_swarm_roll_done_today = false;
int zombie_swarm_duration_hours = 0;
// Invasion mapping: "terrain=enemy" (defaults to bandits when no match)
// Terrain keys: "mountain" for mountain ranges, or regular types like "grass", "snow", "forest", "deep_forest", "stone".
string default_invasion_enemy_type = "bandit";
string[] invasion_terrain_enemy_map = {"mountain=goblin"};
void init_time() {
current_hour = 8;
current_day = 1;
hour_timer.restart();
is_daytime = true;
sun_setting_warned = false;
sunrise_warned = false;
crossfade_active = false;
crossfade_to_night = false;
area_expanded_today = false;
invasion_active = false;
invasion_start_hour = -1;
invasion_chance = 25;
invasion_triggered_today = false;
invasion_roll_done_today = false;
invasion_scheduled_hour = -1;
invasion_enemy_type = "bandit";
invasion_started_once = false;
zombie_swarm_active = false;
zombie_swarm_start_hour = -1;
zombie_swarm_scheduled_hour = -1;
zombie_swarm_triggered_today = false;
zombie_swarm_roll_done_today = false;
zombie_swarm_duration_hours = 0;
reset_player_item_break_state();
update_ambience(true); // Force start
}
void reset_player_item_break_audio_state() {
playerItemBreakMessage = "";
playerItemBreakSoundHandle = -1;
playerItemBreakSoundPending = false;
}
void reset_player_item_break_state() {
playerItemBreakChance = PLAYER_ITEM_BREAK_CHANCE_MIN;
playerItemBreaksToday = 0;
playerItemBreakPending = false;
playerItemBreakPendingType = -1;
reset_player_item_break_audio_state();
}
void queue_player_item_break_message(const string&in message) {
if (message.length() == 0) return;
playerItemBreakMessage = message;
playerItemBreakSoundHandle = p.play_stationary("sounds/items/item_breaks.ogg", false);
playerItemBreakSoundPending = true;
}
void update_player_item_break_message() {
if (!playerItemBreakSoundPending) return;
if (playerItemBreakSoundHandle != -1 && p.sound_is_active(playerItemBreakSoundHandle)) {
return;
}
playerItemBreakSoundHandle = -1;
playerItemBreakSoundPending = false;
if (playerItemBreakMessage.length() > 0) {
speak_with_history(playerItemBreakMessage, true);
playerItemBreakMessage = "";
}
}
void resolve_pending_player_item_break() {
if (!playerItemBreakPending) return;
if (x > BASE_END) return;
if (playerItemBreakPendingType == -1) {
playerItemBreakPending = false;
return;
}
if (remove_breakable_personal_item(playerItemBreakPendingType)) {
cleanup_equipment_after_inventory_change();
string itemName = get_breakable_item_name(playerItemBreakPendingType);
queue_player_item_break_message(itemName + " is worn out so you discard it.");
}
playerItemBreakPending = false;
playerItemBreakPendingType = -1;
}
void attempt_player_item_break_check() {
if (playerItemBreakPending) return;
if (playerItemBreaksToday >= PLAYER_ITEM_BREAKS_PER_DAY) return;
int[] breakableItems;
get_breakable_personal_item_types(breakableItems);
if (breakableItems.length() == 0) return;
int roll = random(1, 100);
int checkChance = int(playerItemBreakChance); // Floor for comparison
if (roll <= checkChance) {
int pickIndex = random(0, int(breakableItems.length()) - 1);
int itemType = breakableItems[pickIndex];
playerItemBreakChance = PLAYER_ITEM_BREAK_CHANCE_MIN;
playerItemBreaksToday++;
if (x <= BASE_END) {
if (remove_breakable_personal_item(itemType)) {
cleanup_equipment_after_inventory_change();
string itemName = get_breakable_item_name(itemType);
queue_player_item_break_message(itemName + " is worn out so you discard it.");
}
} else {
playerItemBreakPending = true;
playerItemBreakPendingType = itemType;
}
} else {
if (playerItemBreakChance < PLAYER_ITEM_BREAK_CHANCE_MAX) {
playerItemBreakChance += PLAYER_ITEM_BREAK_CHANCE_INCREMENT;
}
if (playerItemBreakChance > PLAYER_ITEM_BREAK_CHANCE_MAX) {
playerItemBreakChance = PLAYER_ITEM_BREAK_CHANCE_MAX;
}
}
}
string get_invasion_enemy_type_for_terrain(const string&in terrain_type) {
for (uint i = 0; i < invasion_terrain_enemy_map.length(); i++) {
string entry = invasion_terrain_enemy_map[i];
int eq_pos = entry.find("=");
if (eq_pos <= 0) {
continue;
}
string key = entry.substr(0, eq_pos);
string value = entry.substr(eq_pos + 1);
if (key == terrain_type && value != "") {
return value;
}
}
return default_invasion_enemy_type;
}
string get_invasion_enemy_label(const string&in enemy_type) {
if (enemy_type != "") return enemy_type;
return default_invasion_enemy_type;
}
string get_invasion_enemy_plural(const string&in enemy_type) {
return get_invasion_enemy_label(enemy_type) + "s";
}
string expand_area() {
// Play invasion sound
p.play_stationary("sounds/enemies/invasion.ogg", false);
// 25% chance for mountain, 75% for regular expansion
int type_roll = random(0, 3);
if (type_roll == 0) {
expand_mountain();
return "mountain";
}
return expand_regular_area();
}
string expand_regular_area() {
// Calculate new area
int new_start = MAP_SIZE;
int new_end = MAP_SIZE + EXPANSION_SIZE - 1;
if (expanded_area_start == -1) {
expanded_area_start = new_start;
}
expanded_area_end = new_end;
MAP_SIZE += EXPANSION_SIZE;
// Generate a single terrain type for the entire new area
string terrain_type;
int terrain_roll = random(0, 4);
if (terrain_roll == 0) {
terrain_type = "stone";
} else if (terrain_roll == 1) {
terrain_type = "grass";
} else if (terrain_roll == 2) {
terrain_type = "snow";
} else if (terrain_roll == 3) {
terrain_type = "forest";
} else {
terrain_type = "deep_forest";
}
for (int i = 0; i < EXPANSION_SIZE; i++) {
expanded_terrain_types.insert_last(terrain_type);
}
// Place features based on terrain type
// Forests get trees, other terrain gets streams or trees
bool place_tree = false;
if (terrain_type == "deep_forest") {
// Deep forest: 80% tree, 20% stream
place_tree = (random(1, 100) <= 80);
} else if (terrain_type == "forest") {
// Forest: 60% tree, 40% stream
place_tree = (random(1, 100) <= 60);
} else if (terrain_type == "grass") {
// Grass: 50% tree, 50% stream
place_tree = (random(0, 1) == 0);
} else {
// Stone/snow: no trees, always stream
place_tree = false;
}
if (place_tree) {
// Fill the new area up to its tree cap with proper spacing
spawn_trees(new_start, new_end);
} else {
int stream_width = random(1, 5);
int stream_start = random(0, EXPANSION_SIZE - stream_width);
int actual_start = new_start + stream_start;
add_world_stream(actual_start, stream_width);
}
area_expanded_today = true;
notify("The area has expanded! New territory discovered to the east.");
return terrain_type;
}
void expand_mountain() {
int new_start = MAP_SIZE;
int size = MOUNTAIN_SIZE;
int new_end = new_start + size - 1;
if (expanded_area_start == -1) {
expanded_area_start = new_start;
}
expanded_area_end = new_end;
MAP_SIZE += size;
// Generate mountain range
MountainRange@ mountain = MountainRange(new_start, size);
world_mountains.insert_last(mountain);
// Fill terrain types array for compatibility with save system
for (int i = 0; i < size; i++) {
expanded_terrain_types.insert_last("mountain:" + mountain.terrain_types[i]);
}
// Spawn trees in mountain forest/deep_forest segments
int segment_start = -1;
string segment_terrain = "";
for (int i = 0; i < size; i++) {
string terrain = mountain.terrain_types[i];
if (terrain == "forest" || terrain == "deep_forest") {
if (segment_start == -1) {
segment_start = i;
segment_terrain = terrain;
} else if (terrain != segment_terrain) {
int area_start = new_start + segment_start;
int area_end = new_start + i - 1;
spawn_trees(area_start, area_end);
segment_start = i;
segment_terrain = terrain;
}
} else if (segment_start != -1) {
int area_start = new_start + segment_start;
int area_end = new_start + i - 1;
spawn_trees(area_start, area_end);
segment_start = -1;
segment_terrain = "";
}
}
if (segment_start != -1) {
int area_start = new_start + segment_start;
int area_end = new_start + size - 1;
spawn_trees(area_start, area_end);
}
area_expanded_today = true;
notify("A mountain range has been discovered to the east!");
}
void start_invasion() {
string expansion_terrain = expand_area();
invasion_active = true;
invasion_start_hour = current_hour;
invasion_enemy_type = get_invasion_enemy_type_for_terrain(expansion_terrain);
invasion_started_once = true;
string source = (expansion_terrain == "mountain") ? "the mountains" : "the new area";
string enemy_plural = get_invasion_enemy_plural(invasion_enemy_type);
notify(enemy_plural + " are invading from " + source + "!");
}
void update_invasion_chance_for_new_day() {
if (current_day == 2) {
invasion_chance = 100;
return;
}
if (current_day > 2) {
if (invasion_triggered_today) {
invasion_chance = 25;
} else {
invasion_chance += 25;
if (invasion_chance > 100) invasion_chance = 100;
}
}
}
int get_random_invasion_hour(int min_hour) {
if (min_hour < 6) min_hour = 6;
if (min_hour > 11) return -1;
return random(min_hour, 11);
}
void schedule_invasion() {
if (invasion_scheduled_hour != -1) return;
int hour = get_random_invasion_hour(current_hour);
if (hour == -1) return;
invasion_scheduled_hour = hour;
}
void check_scheduled_invasion() {
if (invasion_active) return;
// Check scheduled invasion regardless of triggered flag (fixes bug where flag was set early in old saves)
if (invasion_scheduled_hour != -1) {
if (current_hour == invasion_scheduled_hour) {
invasion_scheduled_hour = -1;
invasion_triggered_today = true;
start_invasion();
} else if (current_hour > 11) {
invasion_scheduled_hour = -1;
}
return;
}
if (invasion_triggered_today) return;
}
void attempt_daily_invasion() {
if (current_day < 2) return;
if (invasion_triggered_today || invasion_active) return;
if (invasion_roll_done_today) return;
if (current_hour < 6 || current_hour > 12) return;
invasion_roll_done_today = true;
int roll = random(1, 100);
if (roll > invasion_chance) return;
schedule_invasion();
check_scheduled_invasion();
}
bool can_roll_zombie_swarm_today() {
return current_day >= ZOMBIE_SWARM_START_DAY;
}
int get_zombie_swarm_duration_hours() {
int offset = current_day - ZOMBIE_SWARM_START_DAY;
if (offset < 0) offset = 0;
int cycles = offset / ZOMBIE_SWARM_INTERVAL_DAYS;
int duration = ZOMBIE_SWARM_BASE_DURATION_HOURS + (cycles * ZOMBIE_SWARM_DURATION_STEP_HOURS);
if (duration < 1) duration = 1;
return duration;
}
int get_zombie_swarm_chance_for_day() {
int offset = current_day - ZOMBIE_SWARM_START_DAY;
if (offset < 0) offset = 0;
int cycles = offset / ZOMBIE_SWARM_CHANCE_INTERVAL_DAYS;
int chance = ZOMBIE_SWARM_CHANCE_START + (cycles * ZOMBIE_SWARM_CHANCE_STEP);
if (chance > ZOMBIE_SWARM_CHANCE_CAP) chance = ZOMBIE_SWARM_CHANCE_CAP;
if (chance < 0) chance = 0;
return chance;
}
void start_zombie_swarm() {
zombie_swarm_active = true;
zombie_swarm_start_hour = current_hour;
zombie_swarm_duration_hours = get_zombie_swarm_duration_hours();
speak_with_history("A swarm of zombies has been spotted.", true);
}
void end_zombie_swarm() {
zombie_swarm_active = false;
zombie_swarm_start_hour = -1;
zombie_swarm_duration_hours = 0;
}
void check_zombie_swarm_status() {
if (!zombie_swarm_active) return;
int hours_elapsed = current_hour - zombie_swarm_start_hour;
if (hours_elapsed < 0) {
hours_elapsed += 24;
}
if (hours_elapsed >= zombie_swarm_duration_hours) {
end_zombie_swarm();
}
}
void schedule_zombie_swarm() {
if (zombie_swarm_scheduled_hour != -1) return;
int hour = get_random_invasion_hour(current_hour);
if (hour == -1) return;
zombie_swarm_scheduled_hour = hour;
}
void check_scheduled_zombie_swarm() {
if (zombie_swarm_active) return;
if (zombie_swarm_scheduled_hour != -1) {
if (current_hour == zombie_swarm_scheduled_hour) {
zombie_swarm_scheduled_hour = -1;
zombie_swarm_triggered_today = true;
start_zombie_swarm();
} else if (current_hour > 11) {
zombie_swarm_scheduled_hour = -1;
}
return;
}
if (zombie_swarm_triggered_today) return;
}
void attempt_daily_zombie_swarm() {
if (!can_roll_zombie_swarm_today()) return;
if (zombie_swarm_roll_done_today || zombie_swarm_triggered_today || zombie_swarm_active) return;
if (current_hour < 6) return;
zombie_swarm_roll_done_today = true;
int chance = get_zombie_swarm_chance_for_day();
int roll = random(1, 100);
if (roll > chance) {
return;
}
if (current_hour > 11) {
zombie_swarm_triggered_today = true;
start_zombie_swarm();
return;
}
schedule_zombie_swarm();
check_scheduled_zombie_swarm();
}
void get_expanded_area_segments(int[]@ areaStarts, int[]@ areaEnds, string[]@ areaTypes) {
areaStarts.resize(0);
areaEnds.resize(0);
areaTypes.resize(0);
if (expanded_area_start == -1) return;
int total = int(expanded_terrain_types.length());
if (total <= 0) return;
string current_type = get_expanded_area_type(0);
int segment_start = 0;
for (int i = 1; i < total; i++) {
string segment_type = get_expanded_area_type(i);
if (segment_type != current_type) {
areaStarts.insert_last(expanded_area_start + segment_start);
areaEnds.insert_last(expanded_area_start + i - 1);
areaTypes.insert_last(current_type);
segment_start = i;
current_type = segment_type;
}
}
areaStarts.insert_last(expanded_area_start + segment_start);
areaEnds.insert_last(expanded_area_start + total - 1);
areaTypes.insert_last(current_type);
}
void attempt_expansion_roamer_spawn() {
if (!is_daytime) return;
if (invasion_active) return;
if (!invasion_started_once) return;
if (expanded_area_start == -1) return;
if (current_hour % EXPANSION_ROAMER_SPAWN_INTERVAL_HOURS != 0) return;
int[] areaStarts;
int[] areaEnds;
string[] areaTypes;
get_expanded_area_segments(areaStarts, areaEnds, areaTypes);
if (areaStarts.length() == 0) return;
for (uint i = 0; i < areaStarts.length(); i++) {
int count = count_bandits_in_range(areaStarts[i], areaEnds[i]);
if (count >= EXPANSION_ROAMER_MAX_PER_AREA) continue;
string invader_type = get_invasion_enemy_type_for_terrain(areaTypes[i]);
spawn_bandit(areaStarts[i], areaEnds[i], invader_type);
}
}
void attempt_resident_recruitment() {
if (barricade_health <= 0) {
return;
}
// Check if base is full
if (residents_count >= MAX_RESIDENTS) {
return;
}
// Recruitment chance based on storage buildings
int storage_count = world_storages.length();
int min_chance = 50;
int max_chance = 60;
if (storage_count >= 2) {
// 2+ storage: 75-100% chance
min_chance = 75;
max_chance = 100;
} else if (storage_count == 1) {
// 1 storage: 50-75% chance
min_chance = 50;
max_chance = 75;
}
// 0 storage: 50-60% chance (defaults above)
int chance = random(min_chance, max_chance);
int roll = random(1, 100);
if (roll > chance) {
return;
}
int added = random(1, 2);
// Don't exceed cap
if (residents_count + added > MAX_RESIDENTS) {
added = MAX_RESIDENTS - residents_count;
}
if (added <= 0) return;
residents_count += added;
string join_message = (added == 1) ? "A survivor joins your base." : "" + added + " survivors join your base.";
notify(join_message);
// Notify if base is now full
if (residents_count >= MAX_RESIDENTS) {
notify("Your base is at maximum capacity.");
}
}
void end_invasion() {
invasion_active = false;
invasion_start_hour = -1;
transition_bandits_to_wandering();
notify("The " + get_invasion_enemy_label(invasion_enemy_type) + " invasion has ended.");
attempt_resident_recruitment();
}
void transition_bandits_to_wandering() {
for (uint i = 0; i < bandits.length(); i++) {
bandits[i].behavior_state = "wandering";
bandits[i].wander_direction = random(-1, 1);
bandits[i].wander_direction_change_interval = random(BANDIT_WANDER_DIRECTION_CHANGE_MIN, BANDIT_WANDER_DIRECTION_CHANGE_MAX);
bandits[i].wander_direction_timer.restart();
}
}
void check_invasion_status() {
if (!invasion_active) return;
// Check if invasion duration has elapsed
int hours_elapsed = current_hour - invasion_start_hour;
if (hours_elapsed < 0) {
hours_elapsed += 24; // Handle day wrap
}
if (hours_elapsed >= INVASION_DURATION_HOURS) {
end_invasion();
}
}
void manage_bandits_during_invasion() {
// Clear ALL bandits at nighttime (undead eat them)
if (!is_daytime) {
clear_bandits();
return;
}
// During daytime: if invasion is active, maintain invader count
if (invasion_active && expanded_area_start != -1) {
while (bandits.length() < BANDIT_MAX_COUNT) {
spawn_bandit(expanded_area_start, expanded_area_end, invasion_enemy_type);
}
}
// If invasion not active, wandering bandits persist during daytime
}
void update_blessings() {
if (blessing_speed_active && blessing_speed_timer.elapsed >= BLESSING_SPEED_DURATION) {
blessing_speed_active = false;
update_max_health_from_equipment();
speak_with_history("The speed blessing fades.", true);
}
if (blessing_resident_active && blessing_resident_timer.elapsed >= BLESSING_RESIDENT_DURATION) {
blessing_resident_active = false;
speak_with_history("The residents' purpose fades.", true);
}
}
void attempt_blessing() {
if (favor < 1.0) return;
int roll = random(1, 100);
if (roll > BLESSING_TRIGGER_CHANCE) return;
int[] options;
if (player_health < max_health) options.insert_last(0);
if (!blessing_speed_active) options.insert_last(1);
if (barricade_health < BARRICADE_MAX_HEALTH) options.insert_last(2);
if (residents_count > 0 && !blessing_resident_active) options.insert_last(3);
if (options.length() == 0) return;
int choice = options[random(0, options.length() - 1)];
favor -= 1.0;
if (favor < 0) favor = 0;
string[] god_names = {
"Odin's", "Thor's", "Freyja's", "Loki's", "Tyr's", "Baldur's",
"Frigg's", "Heimdall's", "Hel's", "Fenrir's", "Freyr's", "The gods'"
};
string god_name = god_names[random(0, god_names.length() - 1)];
if (choice == 0) {
int before = player_health;
player_health += BLESSING_HEAL_AMOUNT;
if (player_health > max_health) player_health = max_health;
int healed = player_health - before;
string bonus = (healed > 0) ? "You feel restored. +" + healed + " health." : "You feel restored.";
notify(god_name + " favor shines upon you. " + bonus);
} else if (choice == 1) {
blessing_speed_active = true;
blessing_speed_timer.restart();
update_max_health_from_equipment();
notify(god_name + " favor shines upon you. You feel swift for a while.");
} else if (choice == 2) {
int gained = add_barricade_health(BLESSING_BARRICADE_REPAIR);
string bonus = (gained > 0)
? "A divine force repairs the barricade. +" + gained + " health."
: "A divine force surrounds the barricade.";
notify(god_name + " favor shines upon you. " + bonus);
} else if (choice == 3) {
blessing_resident_active = true;
blessing_resident_timer.restart();
notify(god_name + " radiance fills residents with purpose.");
}
}
bool should_attempt_resident_foraging(int hour) {
int residentCount = residents_count;
if (residentCount <= 0) return false;
if (hour == 6) return true;
if (residentCount == 1) return false;
if (residentCount == 2) return hour == 12;
if (residentCount == 3) return hour == 12 || hour == 18;
// 4+ residents: spread across daytime hours
return hour == 10 || hour == 14 || hour == 18;
}
void update_time() {
update_player_item_break_message();
resolve_pending_player_item_break();
if (hour_timer.elapsed >= MS_PER_HOUR) {
hour_timer.restart();
current_hour++;
if (current_hour >= 24) {
current_hour = 0;
current_day++;
area_expanded_today = false; // Reset for new day
update_invasion_chance_for_new_day();
invasion_triggered_today = false;
invasion_roll_done_today = false;
invasion_scheduled_hour = -1;
zombie_swarm_triggered_today = false;
zombie_swarm_scheduled_hour = -1;
zombie_swarm_roll_done_today = false;
quest_roll_done_today = false;
playerItemBreaksToday = 0;
}
// Residents consume food every 8 hours (at midnight, 8am, 4pm)
if (current_hour % 8 == 0) {
consume_food_for_residents();
}
if (current_hour == 18 && !sun_setting_warned) {
notify("The sun is setting.");
sun_setting_warned = true;
} else if (current_hour == 19) {
sun_setting_warned = false;
}
if (current_hour == 5 && !sunrise_warned) {
notify("The sky begins to brighten.");
sunrise_warned = true;
} else if (current_hour == 6) {
sunrise_warned = false;
}
// Check invasion status
check_invasion_status();
check_zombie_swarm_status();
check_ambience_transition();
// Safety: if crossfade failed or was skipped, align day/night with the current hour.
if (!crossfade_active) {
bool expected_daytime = (current_hour >= 6 && current_hour < 19);
if (expected_daytime != is_daytime) {
is_daytime = expected_daytime;
update_ambience(true);
}
}
if (is_daytime && residents_count > 0 && barricade_health < BARRICADE_MAX_HEALTH) {
const int day_start_hour = 6;
const int day_end_hour = 18; // Exclusive for repair scheduling (12-hour window)
if (current_hour >= day_start_hour && current_hour < day_end_hour) {
int repair_window_hours = day_end_hour - day_start_hour;
int interval = repair_window_hours / residents_count;
if (interval < 1) interval = 1;
int day_hour = current_hour - day_start_hour;
if (day_hour % interval == 0) {
if (has_any_storage_food()) {
int gained = add_barricade_health(residents_count * get_resident_effect_multiplier());
if (gained > 0 && x <= BASE_END) {
speak_with_history("Residents repaired the barricade. +" + gained + " health.", true);
}
}
}
}
}
if (current_hour == 6) {
if (undead_residents_pending > 0) {
undead_residents_count += undead_residents_pending;
undead_residents_pending = 0;
}
process_daily_weapon_breakage();
attempt_daily_quest();
attempt_resident_butchering();
attempt_resident_clothing_repairs();
}
if (should_attempt_resident_foraging(current_hour)) {
attempt_resident_foraging();
}
attempt_daily_invasion();
attempt_daily_zombie_swarm();
keep_base_fires_fed();
attempt_player_item_break_check();
update_incense_burning();
attempt_hourly_flying_creature_spawn();
attempt_hourly_boar_spawn();
attempt_hourly_wight_spawn();
attempt_hourly_vampyr_spawn();
check_scheduled_invasion();
check_scheduled_zombie_swarm();
attempt_expansion_roamer_spawn();
attempt_blessing();
check_weather_transition();
attempt_resident_collection();
attempt_resident_snare_retrieval();
attempt_resident_fishing();
attempt_resident_fish_smoking();
attempt_livestock_production();
if (current_hour == 6) {
save_game_state();
}
}
ensure_ambience_running();
// Proactive resident ranged defense
attempt_resident_ranged_defense();
// Manage invasion enemies during active invasion
manage_bandits_during_invasion();
}
void update_incense_burning() {
if (!incense_burning || incense_hours_remaining <= 0) return;
favor += INCENSE_FAVOR_PER_HOUR;
incense_hours_remaining--;
if (incense_hours_remaining <= 0) {
incense_hours_remaining = 0;
incense_burning = false;
notify("The incense has burned out.");
}
}
void check_time_input() {
if (key_pressed(KEY_T)) {
speak_with_history(get_time_string(), true);
}
}
string get_time_string() {
int display_hour = current_hour;
string period = "am";
if (display_hour == 0) {
display_hour = 12;
} else if (display_hour == 12) {
period = "pm";
} else if (display_hour > 12) {
display_hour -= 12;
period = "pm";
}
return display_hour + " oclock " + period + " day " + current_day;
}
void check_ambience_transition() {
// Day is 6 (6AM) to 18 (6PM inclusive, so transition starts at hour 18)
// Night is 19 (7PM) to 5 (5AM inclusive, so transition starts at hour 5)
// Crossfade begins at hour 18 (sunset) and hour 5 (sunrise)
// Start crossfade to night at hour 18
if (current_hour == 18 && is_daytime && !crossfade_active) {
start_crossfade(true); // Fade to night
}
// Start crossfade to day at hour 5
else if (current_hour == 5 && !is_daytime && !crossfade_active) {
start_crossfade(false); // Fade to day
}
}
void start_crossfade(bool to_night) {
crossfade_active = true;
crossfade_to_night = to_night;
crossfade_timer.restart();
// Start the incoming sound at minimum volume (silent)
if (to_night) {
// Starting night sound
if (night_sound_handle == -1 || !p.sound_is_active(night_sound_handle)) {
// Clean up invalid handle if it exists
if (night_sound_handle != -1) {
p.destroy_sound(night_sound_handle);
night_sound_handle = -1;
}
night_sound_handle = p.play_stationary("sounds/nature/night.ogg", true);
}
p.update_sound_start_values(night_sound_handle, 0.0, CROSSFADE_MIN_VOLUME, 1.0);
} else {
// Starting day sound
if (day_sound_handle == -1 || !p.sound_is_active(day_sound_handle)) {
// Clean up invalid handle if it exists
if (day_sound_handle != -1) {
p.destroy_sound(day_sound_handle);
day_sound_handle = -1;
}
day_sound_handle = p.play_stationary("sounds/nature/day.ogg", true);
}
p.update_sound_start_values(day_sound_handle, 0.0, CROSSFADE_MIN_VOLUME, 1.0);
}
}
void update_crossfade() {
if (!crossfade_active) return;
// If a handle went inactive mid-fade, cancel and restart ambience to avoid fading unrelated sounds.
if (crossfade_to_night) {
if (day_sound_handle == -1 || !p.sound_is_active(day_sound_handle)) {
crossfade_active = false;
is_daytime = false;
update_ambience(true);
return;
}
if (night_sound_handle == -1 || !p.sound_is_active(night_sound_handle)) {
crossfade_active = false;
is_daytime = false;
update_ambience(true);
return;
}
} else {
if (night_sound_handle == -1 || !p.sound_is_active(night_sound_handle)) {
crossfade_active = false;
is_daytime = true;
update_ambience(true);
return;
}
if (day_sound_handle == -1 || !p.sound_is_active(day_sound_handle)) {
crossfade_active = false;
is_daytime = true;
update_ambience(true);
return;
}
}
float progress = float(crossfade_timer.elapsed) / float(CROSSFADE_DURATION);
if (progress > 1.0) progress = 1.0;
// Volume interpolation: use a slow-start curve to make fade-outs more gradual
float volume_range = CROSSFADE_MAX_VOLUME - CROSSFADE_MIN_VOLUME; // dB range
float eased_progress = progress * progress;
float fade_out_vol = CROSSFADE_MAX_VOLUME - (volume_range * eased_progress); // 0 -> min
float fade_in_vol = CROSSFADE_MIN_VOLUME + (volume_range * eased_progress); // min -> 0
if (crossfade_to_night) {
// Fading day out, night in
if (day_sound_handle != -1) p.update_sound_start_values(day_sound_handle, 0.0, fade_out_vol, 1.0);
if (night_sound_handle != -1) p.update_sound_start_values(night_sound_handle, 0.0, fade_in_vol, 1.0);
} else {
// Fading night out, day in
if (night_sound_handle != -1) p.update_sound_start_values(night_sound_handle, 0.0, fade_out_vol, 1.0);
if (day_sound_handle != -1) p.update_sound_start_values(day_sound_handle, 0.0, fade_in_vol, 1.0);
}
// Complete the crossfade
if (progress >= 1.0) {
complete_crossfade();
}
}
void complete_crossfade() {
crossfade_active = false;
if (crossfade_to_night) {
// Destroy day sound, ensure night is at full volume
if (day_sound_handle != -1) {
p.destroy_sound(day_sound_handle);
day_sound_handle = -1;
}
if (night_sound_handle != -1) p.update_sound_start_values(night_sound_handle, 0.0, 0.0, 1.0);
is_daytime = false;
if (night_sound_handle == -1 || !p.sound_is_active(night_sound_handle)) {
update_ambience(false);
}
} else {
// Destroy night sound, ensure day is at full volume
if (night_sound_handle != -1) {
p.destroy_sound(night_sound_handle);
night_sound_handle = -1;
}
if (day_sound_handle != -1) p.update_sound_start_values(day_sound_handle, 0.0, 0.0, 1.0);
is_daytime = true;
if (day_sound_handle == -1 || !p.sound_is_active(day_sound_handle)) {
update_ambience(false);
}
}
}
void ensure_ambience_running() {
if (crossfade_active) return;
if (is_daytime) {
if (day_sound_handle == -1 || !p.sound_is_active(day_sound_handle)) {
update_ambience(false);
}
} else {
if (night_sound_handle == -1 || !p.sound_is_active(night_sound_handle)) {
update_ambience(false);
}
}
}
void update_ambience(bool force_restart) {
// When force_restart is true, clean up both handles first
if (force_restart) {
if (day_sound_handle != -1) {
p.destroy_sound(day_sound_handle);
day_sound_handle = -1;
}
if (night_sound_handle != -1) {
p.destroy_sound(night_sound_handle);
night_sound_handle = -1;
}
}
if (is_daytime) {
// Stop night ambience if playing
if (night_sound_handle != -1) {
p.destroy_sound(night_sound_handle);
night_sound_handle = -1;
}
// Start day ambience if not already playing
if (day_sound_handle == -1 || !p.sound_is_active(day_sound_handle)) {
if (day_sound_handle != -1) {
p.destroy_sound(day_sound_handle);
day_sound_handle = -1;
}
day_sound_handle = p.play_stationary("sounds/nature/day.ogg", true);
}
} else {
// Stop day ambience if playing
if (day_sound_handle != -1) {
p.destroy_sound(day_sound_handle);
day_sound_handle = -1;
}
// Start night ambience if not already playing
if (night_sound_handle == -1 || !p.sound_is_active(night_sound_handle)) {
if (night_sound_handle != -1) {
p.destroy_sound(night_sound_handle);
night_sound_handle = -1;
}
night_sound_handle = p.play_stationary("sounds/nature/night.ogg", true);
}
}
}