1046 lines
34 KiB
Plaintext
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);
|
|
}
|
|
}
|
|
}
|