// 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; // 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; 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; update_ambience(true); // Force start } void expand_area() { // Play invasion sound p.play_stationary("sounds/enemies/invasion.ogg", false); // 25% chance for mountain, 75% for regular expansion int type_roll = random(0, 3); if (type_roll == 0) { expand_mountain(); } else { expand_regular_area(); } } void expand_regular_area() { // Calculate new area int new_start = MAP_SIZE; int new_end = MAP_SIZE + EXPANSION_SIZE - 1; 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 exactly one feature: either a stream or a tree bool place_stream = (terrain_type != "grass") || (random(0, 1) == 0); if (place_stream) { 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); } else { // Try to place a tree with proper spacing and per-area limits spawn_tree_in_area(new_start, new_end); } area_expanded_today = true; notify("The area has expanded! New territory discovered to the east."); } void expand_mountain() { int new_start = MAP_SIZE; int size = MOUNTAIN_SIZE; int new_end = new_start + size - 1; if (expanded_area_start == -1) { expanded_area_start = new_start; } expanded_area_end = new_end; MAP_SIZE += size; // Generate mountain range MountainRange@ mountain = MountainRange(new_start, size); world_mountains.insert_last(mountain); // Fill terrain types array for compatibility with save system for (int i = 0; i < size; i++) { expanded_terrain_types.insert_last("mountain:" + mountain.terrain_types[i]); } area_expanded_today = true; notify("A mountain range has been discovered to the east!"); } void start_invasion() { expand_area(); invasion_active = true; invasion_start_hour = current_hour; notify("Bandits are invading from the new area!"); } 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(); } 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, 3); // 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 bandit 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 bandit count if (invasion_active && expanded_area_start != -1) { while (bandits.length() < BANDIT_MAX_COUNT) { spawn_bandit(expanded_area_start, expanded_area_end); } } // 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); } } 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 (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); } } void update_time() { 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; quest_roll_done_today = false; 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("Sunrise isn't far away."); sunrise_warned = true; } else if (current_hour == 6) { sunrise_warned = false; } // Check invasion status check_invasion_status(); check_ambience_transition(); if (is_daytime && residents_count > 0 && barricade_health < BARRICADE_MAX_HEALTH && current_hour % 4 == 0) { if (get_storage_count(ITEM_MEAT) > 0) { int gained = add_barricade_health(residents_count); if (gained > 0 && x <= BASE_END) { speak_with_history("Residents repaired the barricade. +" + gained + " health.", true); } } } if (current_hour == 6) { process_daily_weapon_breakage(); attempt_daily_quest(); save_game_state(); } attempt_daily_invasion(); keep_base_fires_fed(); update_incense_burning(); attempt_hourly_flying_creature_spawn(); attempt_hourly_boar_spawn(); check_scheduled_invasion(); attempt_blessing(); check_weather_transition(); attempt_resident_collection(); } // Proactive resident defense with slings attempt_resident_sling_defense(); // Manage bandits 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; 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; } 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; } } 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); } } }