Files
draugnorak/src/time_system.nvgt
2026-01-23 21:09:54 -05:00

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