// 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); } } }