diff --git a/lang/af.ini b/lang/af.ini index 8eaeba3..607ad1c 100644 --- a/lang/af.ini +++ b/lang/af.ini @@ -117,6 +117,7 @@ time.event.mountain_discovered='n Bergreeks is in die ooste ontdek! time.event.invasion_source_mountains=die berge time.event.invasion_source_new_area=die nuwe area time.event.invasion_start={enemy_plural} val in vanaf {source}! +time.event.undead_resident_raid=Die dooies onthou waar mense nog leef. {count} ondode inwoners kom. time.event.zombie_swarm_spotted='n Swerm zombies is gewaar. time.event.survivor_joins_one='n Oorlewende sluit by jou basis aan. time.event.survivor_joins_many={count} oorlewendes sluit by jou basis aan. @@ -1031,7 +1032,7 @@ system.goals.mastery.crafting.name=Handwerk Bemeestering system.goals.mastery.leadership.level1=Inwoners is versigtig en breek minder gereedskap en wapens. system.goals.mastery.leadership.level2=Inwoners is georganiseer en slaag meer gereeld met werk. system.goals.mastery.leadership.level3=Inwoners tree vinniger op tydens verdediging en dringende werk. -system.goals.mastery.leadership.level4=Inwoners rantsoeneer kos en eet elke 12 uur in plaas van elke 8. +system.goals.mastery.leadership.level4=Inwoners rantsoeneer kos en bly werk na nagval. system.goals.mastery.leadership.name=Leierskap Bemeestering system.goals.mastery.taming.level1=Troeteldiere kan gevalle items haal. system.goals.mastery.taming.level2=Troeteldiere is sterker en kry een maksimum gesondheid. diff --git a/lang/en.ini b/lang/en.ini index 2848b0c..ab43949 100644 --- a/lang/en.ini +++ b/lang/en.ini @@ -71,6 +71,7 @@ time.event.mountain_discovered=A mountain range has been discovered to the east! time.event.invasion_source_mountains=the mountains time.event.invasion_source_new_area=the new area time.event.invasion_start={enemy_plural} are invading from {source}! +time.event.undead_resident_raid=The dead remember where people still live. {count} undead residents are coming. time.event.zombie_swarm_spotted=A swarm of zombies has been spotted. time.event.survivor_joins_one=A survivor joins your base. time.event.survivor_joins_many={count} survivors join your base. @@ -293,7 +294,7 @@ goals.mastery.crafting.level4=Personal tools, weapons, and clothing no longer we goals.mastery.leadership.level1=Residents are careful and break fewer tools and weapons. goals.mastery.leadership.level2=Residents are organized and succeed at work more often. goals.mastery.leadership.level3=Residents act faster during defense and urgent work. -goals.mastery.leadership.level4=Residents ration food, eating every 12 hours instead of every 8. +goals.mastery.leadership.level4=Residents ration food and keep working after nightfall. goals.mastery.taming.level1=Pets can retrieve dropped items. goals.mastery.taming.level2=Pets are heartier and gain one maximum health. goals.mastery.taming.level3=Pets recover from knockout faster. @@ -976,7 +977,7 @@ msg.5cac31311668=Pasture is full. No livestock were recovered. msg.5cc7429297c8=that can be destroyed with an axe. ; seed:learn_sounds_label:sounds/actions/cast_strength.ogg msg.5dfc869b7588=cast strength -; src/time_system.nvgt:801:notify[0] +; src/time_system.nvgt:861:notify[0] msg.5ea84b1201bb={arg1} favor grants you the eyes of an eagle. ; seed:learn_sounds_label:sounds/menu/menu_select.ogg ; seed:learn_sounds_label:sounds/menu.bak/menu_select.ogg @@ -1070,8 +1071,8 @@ msg.803e4c41bbd2=sticks msg.80655da8d80a=tree ; src/item_registry.nvgt:item_plural:canoes msg.806baaf8e039=canoes -; src/time_system.nvgt:783:notify[0] -; src/time_system.nvgt:793:notify[0] +; src/time_system.nvgt:843:notify[0] +; src/time_system.nvgt:853:notify[0] msg.81df04a81e24={arg1} favor shines upon you. {arg2} ; src/quests/catch_the_boomerang_game.nvgt:22:insert_last[0] msg.823be948275f=- Better timing earns more points (up to 4) @@ -1097,7 +1098,7 @@ msg.86e257c3efcb=Oldest notification. {arg1} msg.87383ce4344d=Bowstrings ; src/item_registry.nvgt:item_plural:ropes msg.87cdb5c91437=ropes -; src/time_system.nvgt:797:notify[0] +; src/time_system.nvgt:857:notify[0] msg.8818f1d55166={arg1} radiance fills residents with purpose. ; src/quests/bat_invasion_game.nvgt:7:insert_last[0] msg.88749dbbbf09==== Bat Invasion === @@ -1115,7 +1116,7 @@ msg.8f993ac20023=You enter a narrow mountain pass. A massive Unicorn blocks your msg.917ee46db0cd=bee ; seed:learn_sounds_label:sounds/player_male_damage.ogg msg.924e0eb6b6f1=player male damage -; src/time_system.nvgt:788:notify[0] +; src/time_system.nvgt:848:notify[0] msg.9281d2fe5f08={arg1} favor shines upon you. You feel swift for a while. ; seed:learn_sounds_label:sounds/actions/climb_rope.ogg msg.92abc6d6953f=climb rope diff --git a/lang/en.template.ini b/lang/en.template.ini index 2848b0c..ab43949 100644 --- a/lang/en.template.ini +++ b/lang/en.template.ini @@ -71,6 +71,7 @@ time.event.mountain_discovered=A mountain range has been discovered to the east! time.event.invasion_source_mountains=the mountains time.event.invasion_source_new_area=the new area time.event.invasion_start={enemy_plural} are invading from {source}! +time.event.undead_resident_raid=The dead remember where people still live. {count} undead residents are coming. time.event.zombie_swarm_spotted=A swarm of zombies has been spotted. time.event.survivor_joins_one=A survivor joins your base. time.event.survivor_joins_many={count} survivors join your base. @@ -293,7 +294,7 @@ goals.mastery.crafting.level4=Personal tools, weapons, and clothing no longer we goals.mastery.leadership.level1=Residents are careful and break fewer tools and weapons. goals.mastery.leadership.level2=Residents are organized and succeed at work more often. goals.mastery.leadership.level3=Residents act faster during defense and urgent work. -goals.mastery.leadership.level4=Residents ration food, eating every 12 hours instead of every 8. +goals.mastery.leadership.level4=Residents ration food and keep working after nightfall. goals.mastery.taming.level1=Pets can retrieve dropped items. goals.mastery.taming.level2=Pets are heartier and gain one maximum health. goals.mastery.taming.level3=Pets recover from knockout faster. @@ -976,7 +977,7 @@ msg.5cac31311668=Pasture is full. No livestock were recovered. msg.5cc7429297c8=that can be destroyed with an axe. ; seed:learn_sounds_label:sounds/actions/cast_strength.ogg msg.5dfc869b7588=cast strength -; src/time_system.nvgt:801:notify[0] +; src/time_system.nvgt:861:notify[0] msg.5ea84b1201bb={arg1} favor grants you the eyes of an eagle. ; seed:learn_sounds_label:sounds/menu/menu_select.ogg ; seed:learn_sounds_label:sounds/menu.bak/menu_select.ogg @@ -1070,8 +1071,8 @@ msg.803e4c41bbd2=sticks msg.80655da8d80a=tree ; src/item_registry.nvgt:item_plural:canoes msg.806baaf8e039=canoes -; src/time_system.nvgt:783:notify[0] -; src/time_system.nvgt:793:notify[0] +; src/time_system.nvgt:843:notify[0] +; src/time_system.nvgt:853:notify[0] msg.81df04a81e24={arg1} favor shines upon you. {arg2} ; src/quests/catch_the_boomerang_game.nvgt:22:insert_last[0] msg.823be948275f=- Better timing earns more points (up to 4) @@ -1097,7 +1098,7 @@ msg.86e257c3efcb=Oldest notification. {arg1} msg.87383ce4344d=Bowstrings ; src/item_registry.nvgt:item_plural:ropes msg.87cdb5c91437=ropes -; src/time_system.nvgt:797:notify[0] +; src/time_system.nvgt:857:notify[0] msg.8818f1d55166={arg1} radiance fills residents with purpose. ; src/quests/bat_invasion_game.nvgt:7:insert_last[0] msg.88749dbbbf09==== Bat Invasion === @@ -1115,7 +1116,7 @@ msg.8f993ac20023=You enter a narrow mountain pass. A massive Unicorn blocks your msg.917ee46db0cd=bee ; seed:learn_sounds_label:sounds/player_male_damage.ogg msg.924e0eb6b6f1=player male damage -; src/time_system.nvgt:788:notify[0] +; src/time_system.nvgt:848:notify[0] msg.9281d2fe5f08={arg1} favor shines upon you. You feel swift for a while. ; seed:learn_sounds_label:sounds/actions/climb_rope.ogg msg.92abc6d6953f=climb rope diff --git a/lang/es.ini b/lang/es.ini index 36bff3d..6c9d3c4 100644 --- a/lang/es.ini +++ b/lang/es.ini @@ -117,6 +117,7 @@ time.event.mountain_discovered=Se ha descubierto una cordillera al este! time.event.invasion_source_mountains=las montanas time.event.invasion_source_new_area=el area nueva time.event.invasion_start={enemy_plural} estan invadiendo desde {source}! +time.event.undead_resident_raid=Los muertos recuerdan donde aun vive la gente. Vienen {count} residentes no muertos. time.event.zombie_swarm_spotted=Se ha detectado una horda de zombis. time.event.survivor_joins_one=Un superviviente se une a tu base. time.event.survivor_joins_many={count} supervivientes se unen a tu base. @@ -1031,7 +1032,7 @@ system.goals.mastery.crafting.name=Maestría de Fabricación system.goals.mastery.leadership.level1=Los residentes son cuidadosos y rompen menos herramientas y armas. system.goals.mastery.leadership.level2=Los residentes son organizados y tienen éxito en el trabajo más a menudo. system.goals.mastery.leadership.level3=Los residentes actúan más rápido durante la defensa y el trabajo urgente. -system.goals.mastery.leadership.level4=Los residentes racionan comida y comen cada 12 horas en vez de cada 8. +system.goals.mastery.leadership.level4=Los residentes racionan comida y siguen trabajando después del anochecer. system.goals.mastery.leadership.name=Maestría de Liderazgo system.goals.mastery.taming.level1=Las mascotas pueden recuperar objetos caídos. system.goals.mastery.taming.level2=Las mascotas son más resistentes y ganan una salud máxima. diff --git a/scripts/generate_i18n_catalog.py b/scripts/generate_i18n_catalog.py index 4b76303..3ac772e 100644 --- a/scripts/generate_i18n_catalog.py +++ b/scripts/generate_i18n_catalog.py @@ -562,6 +562,8 @@ def write_catalog(entries: Dict[str, Dict[str, object]], output_path: Path) -> N ("system.time.event.invasion_source_mountains", "the mountains"), ("system.time.event.invasion_source_new_area", "the new area"), ("system.time.event.invasion_start", "{enemy_plural} are invading from {source}!"), + ("system.time.event.undead_resident_raid", + "The dead remember where people still live. {count} undead residents are coming."), ("system.time.event.zombie_swarm_spotted", "A swarm of zombies has been spotted."), ("system.time.event.survivor_joins_one", "A survivor joins your base."), ("system.time.event.survivor_joins_many", "{count} survivors join your base."), @@ -796,7 +798,7 @@ def write_catalog(entries: Dict[str, Dict[str, object]], output_path: Path) -> N ("system.goals.mastery.leadership.level1", "Residents are careful and break fewer tools and weapons."), ("system.goals.mastery.leadership.level2", "Residents are organized and succeed at work more often."), ("system.goals.mastery.leadership.level3", "Residents act faster during defense and urgent work."), - ("system.goals.mastery.leadership.level4", "Residents ration food, eating every 12 hours instead of every 8."), + ("system.goals.mastery.leadership.level4", "Residents ration food and keep working after nightfall."), ("system.goals.mastery.taming.level1", "Pets can retrieve dropped items."), ("system.goals.mastery.taming.level2", "Pets are heartier and gain one maximum health."), ("system.goals.mastery.taming.level3", "Pets recover from knockout faster."), diff --git a/src/base_system.nvgt b/src/base_system.nvgt index fd98ca8..8675975 100644 --- a/src/base_system.nvgt +++ b/src/base_system.nvgt @@ -155,7 +155,7 @@ void consume_food_for_residents() { } void attempt_resident_fishing() { - if (!is_daytime) + if (!is_daytime && !leadership_mastery_allows_night_work()) return; if (residents_count <= 0) return; @@ -216,7 +216,7 @@ void attempt_resident_fishing() { } void attempt_resident_fish_smoking() { - if (!is_daytime) + if (!is_daytime && !leadership_mastery_allows_night_work()) return; if (residents_count <= 0) return; @@ -741,8 +741,7 @@ void attempt_resident_clothing_repairs() { // Resident snare retrieval void attempt_resident_snare_retrieval() { - // Only during daytime - if (!is_daytime) + if (!is_daytime && !leadership_mastery_allows_night_work()) return; // Need residents @@ -926,8 +925,7 @@ void attempt_resident_butchering() { // Resident resource collection void attempt_resident_collection() { - // Only during daytime - if (!is_daytime) + if (!is_daytime && !leadership_mastery_allows_night_work()) return; // Need residents @@ -1005,8 +1003,7 @@ void attempt_resident_collection() { // Resident foraging - produces baskets of fruits and nuts from reed baskets void attempt_resident_foraging() { - // Only during daytime - if (!is_daytime) + if (!is_daytime && !leadership_mastery_allows_night_work()) return; // Need residents diff --git a/src/constants.nvgt b/src/constants.nvgt index 903186c..dd8d0e6 100644 --- a/src/constants.nvgt +++ b/src/constants.nvgt @@ -86,6 +86,9 @@ const int ZOMBIE_ATTACK_MAX_HEIGHT = 6; const int UNDEAD_RESIDENT_HEALTH = 16; const int UNDEAD_RESIDENT_DAMAGE_MIN = 5; const int UNDEAD_RESIDENT_DAMAGE_MAX = 7; +const int UNDEAD_RESIDENT_RAID_BASE_COUNT = 2; +const int UNDEAD_RESIDENT_RAID_DAYS_PER_EXTRA = 18; +const int UNDEAD_RESIDENT_RAID_MAX_COUNT = 8; // Wight settings (undead elite) const int WIGHT_HEALTH = 40; diff --git a/src/enemies/corpse_worm.nvgt b/src/enemies/corpse_worm.nvgt new file mode 100644 index 0000000..763bc18 --- /dev/null +++ b/src/enemies/corpse_worm.nvgt @@ -0,0 +1,222 @@ +// Corpse worms are weak opportunistic enemies that begin appearing after day 1. + +string[] corpse_worm_sounds = {"sounds/enemies/corpse_worm.ogg"}; +timer corpse_worm_spawn_timer; + +class CorpseWorm { + int position; + int health; + int sound_handle; + bool in_weapon_range; + timer move_timer; + timer attack_timer; + string voice_sound; + + CorpseWorm(int pos) { + position = pos; + health = CORPSE_WORM_HEALTH; + sound_handle = -1; + in_weapon_range = false; + voice_sound = corpse_worm_sounds[random(0, corpse_worm_sounds.length() - 1)]; + move_timer.restart(); + attack_timer.restart(); + } +} CorpseWorm @[] corpse_worms; + +void update_corpse_worm_weapon_range_audio() { + for (uint i = 0; i < corpse_worms.length(); i++) { + update_weapon_range_audio(corpse_worms[i].position, corpse_worms[i].in_weapon_range); + } +} +bool corpse_worm_range_audio_registered = false; + +void ensure_corpse_worm_range_audio_registration() { + if (corpse_worm_range_audio_registered) + return; + corpse_worm_range_audio_registered = register_weapon_range_audio_callback(@update_corpse_worm_weapon_range_audio); +} + +void clear_corpse_worms() { + for (uint i = 0; i < corpse_worms.length(); i++) { + force_weapon_range_exit(corpse_worms[i].position, corpse_worms[i].in_weapon_range); + if (corpse_worms[i].sound_handle != -1) { + p.destroy_sound(corpse_worms[i].sound_handle); + corpse_worms[i].sound_handle = -1; + } + } + corpse_worms.resize(0); + corpse_worm_spawn_timer.restart(); +} + +CorpseWorm @get_corpse_worm_at(int pos) { + for (uint i = 0; i < corpse_worms.length(); i++) { + if (corpse_worms[i].position == pos) { + return @corpse_worms[i]; + } + } + return null; +} + +bool can_spawn_corpse_worm_at(int pos) { + if (pos <= BASE_END || pos < 0 || pos >= MAP_SIZE) + return false; + if (pos == x) + return false; + if (get_corpse_worm_at(pos) != null) + return false; + if (get_bandit_at(pos) != null) + return false; + if (get_boar_at(pos) != null) + return false; + if (get_zombie_at(pos) != null) + return false; + if (get_flying_creature_at(pos) != null) + return false; + return true; +} + +int pick_corpse_worm_spawn_position() { + for (int attempts = 0; attempts < 20; attempts++) { + int distance = random(CORPSE_WORM_SPAWN_MIN_DISTANCE, CORPSE_WORM_SPAWN_MAX_DISTANCE); + int direction = (random(0, 1) == 0) ? -1 : 1; + int candidate = x + (distance * direction); + if (can_spawn_corpse_worm_at(candidate)) + return candidate; + } + + for (int candidate = BASE_END + 1; candidate < MAP_SIZE; candidate++) { + if (can_spawn_corpse_worm_at(candidate)) + return candidate; + } + + return -1; +} + +void spawn_corpse_worm() { + int spawn_x = pick_corpse_worm_spawn_position(); + if (spawn_x == -1) + return; + + CorpseWorm @worm = CorpseWorm(spawn_x); + corpse_worms.insert_last(worm); + + int[] areaStarts; + int[] areaEnds; + get_active_audio_areas(areaStarts, areaEnds); + if (areaStarts.length() == 0 || range_overlaps_active_areas(spawn_x, spawn_x, areaStarts, areaEnds)) { + worm.sound_handle = + play_1d_with_volume_step(worm.voice_sound, x, spawn_x, true, CORPSE_WORM_SOUND_VOLUME_STEP); + } +} + +void attempt_corpse_worm_spawn() { + if (current_day < CORPSE_WORM_START_DAY) + return; + if (x <= BASE_END) + return; + if (corpse_worms.length() >= CORPSE_WORM_MAX_COUNT) + return; + if (corpse_worm_spawn_timer.elapsed < CORPSE_WORM_SPAWN_ROLL_INTERVAL) + return; + + corpse_worm_spawn_timer.restart(); + if (random(1, 100) <= CORPSE_WORM_SPAWN_CHANCE) { + spawn_corpse_worm(); + } +} + +bool try_attack_player_corpse_worm(CorpseWorm @worm) { + if (player_health <= 0) + return false; + if (x <= BASE_END) + return false; + if (y != 0) + return false; + if (worm.position != x) + return false; + if (worm.attack_timer.elapsed < CORPSE_WORM_ATTACK_INTERVAL) + return false; + + worm.attack_timer.restart(); + player_health -= CORPSE_WORM_DAMAGE; + if (player_health < 0) + player_health = 0; + play_player_damage_sound(); + return true; +} + +void update_corpse_worm(CorpseWorm @worm, bool audio_active) { + if (!audio_active) { + if (worm.sound_handle != -1) { + p.destroy_sound(worm.sound_handle); + worm.sound_handle = -1; + } + } else if (worm.sound_handle != -1 && p.sound_is_active(worm.sound_handle)) { + p.update_sound_1d(worm.sound_handle, worm.position); + } else if (worm.sound_handle == -1 || !p.sound_is_active(worm.sound_handle)) { + if (worm.sound_handle != -1) { + p.destroy_sound(worm.sound_handle); + } + worm.sound_handle = + play_1d_with_volume_step(worm.voice_sound, x, worm.position, true, CORPSE_WORM_SOUND_VOLUME_STEP); + } + + if (try_attack_player_corpse_worm(worm)) + return; + if (worm.move_timer.elapsed < CORPSE_WORM_MOVE_INTERVAL) + return; + + worm.move_timer.restart(); + int target_x = (x > BASE_END) ? x : BASE_END + 1; + if (target_x == worm.position) + return; + + int direction = (target_x > worm.position) ? 1 : -1; + int next_pos = worm.position + direction; + if (next_pos <= BASE_END || next_pos < 0 || next_pos >= MAP_SIZE) + return; + if (get_corpse_worm_at(next_pos) != null) + return; + + worm.position = next_pos; + if (audio_active) { + play_creature_footstep(x, worm.position, BASE_END, GRASS_END, CORPSE_WORM_FOOTSTEP_MAX_DISTANCE, + CORPSE_WORM_SOUND_VOLUME_STEP); + } +} + +void update_corpse_worms() { + ensure_corpse_worm_range_audio_registration(); + attempt_corpse_worm_spawn(); + + int[] areaStarts; + int[] areaEnds; + get_active_audio_areas(areaStarts, areaEnds); + bool limit_audio = (areaStarts.length() > 0); + for (uint i = 0; i < corpse_worms.length(); i++) { + bool audio_active = + !limit_audio || range_overlaps_active_areas(corpse_worms[i].position, corpse_worms[i].position, areaStarts, + areaEnds); + update_corpse_worm(corpse_worms[i], audio_active); + } +} + +bool damage_corpse_worm_at(int pos, int damage) { + for (uint i = 0; i < corpse_worms.length(); i++) { + if (corpse_worms[i].position == pos) { + corpse_worms[i].health -= damage; + if (corpse_worms[i].health <= 0) { + force_weapon_range_exit(corpse_worms[i].position, corpse_worms[i].in_weapon_range); + if (corpse_worms[i].sound_handle != -1) { + p.destroy_sound(corpse_worms[i].sound_handle); + corpse_worms[i].sound_handle = -1; + } + play_creature_death_sounds("sounds/enemies/enemy_falls.ogg", corpse_worms[i].voice_sound, x, pos, + CORPSE_WORM_SOUND_VOLUME_STEP); + corpse_worms.remove_at(i); + } + return true; + } + } + return false; +} diff --git a/src/enemies/undead.nvgt b/src/enemies/undead.nvgt index dbac127..46cde7e 100644 --- a/src/enemies/undead.nvgt +++ b/src/enemies/undead.nvgt @@ -79,6 +79,7 @@ class Undead { bool retreating; bool suppress_voice; bool should_despawn; + bool counts_as_lost_resident; timer move_timer; timer attack_timer; @@ -92,6 +93,7 @@ class Undead { retreating = false; suppress_voice = false; should_despawn = false; + counts_as_lost_resident = (undead_type == "undead_resident"); move_timer.restart(); attack_timer.restart(); } @@ -257,7 +259,7 @@ int pick_undead_spawn_near_player(int min_distance, int max_distance) { return candidate; } -void spawn_undead(const string& in undead_type = "zombie") { +void spawn_undead(const string& in undead_type = "zombie", bool counts_as_lost_resident = true) { int spawn_x = -1; if (undead_type == "zombie" || undead_type == "undead_resident") { spawn_x = pick_undead_spawn_near_player(ZOMBIE_SPAWN_MIN_DISTANCE, ZOMBIE_SPAWN_MAX_DISTANCE); @@ -269,6 +271,7 @@ void spawn_undead(const string& in undead_type = "zombie") { return; Undead @undead = Undead(spawn_x, undead_type); + undead.counts_as_lost_resident = (undead_type == "undead_resident" && counts_as_lost_resident); undeads.insert_last(undead); // Play looping sound that follows the undead int[] areaStarts; @@ -526,7 +529,7 @@ void update_undeads() { for (uint i = 0; i < undeads.length(); i++) { if (undeads[i].undead_type == "zombie") { zombie_count++; - } else if (undeads[i].undead_type == "undead_resident") { + } else if (undeads[i].undead_type == "undead_resident" && undeads[i].counts_as_lost_resident) { undead_resident_count++; } } @@ -566,7 +569,7 @@ void update_undeads() { } void attempt_hourly_wight_spawn() { - if (current_hour == 19) { + if (current_hour == get_current_night_start_hour()) { wight_spawned_this_night_count = 0; } @@ -602,7 +605,7 @@ void attempt_hourly_wight_spawn() { } void attempt_hourly_vampyr_spawn() { - if (current_hour == 19) { + if (current_hour == get_current_night_start_hour()) { vampyr_spawned_this_night_count = 0; } @@ -649,7 +652,7 @@ bool damage_undead_at(int pos, int damage) { if (world_altars.length() > 0) { favor += 0.2; } - if (undeads[i].undead_type == "undead_resident") { + if (undeads[i].undead_type == "undead_resident" && undeads[i].counts_as_lost_resident) { undead_residents_count--; if (undead_residents_count < 0) undead_residents_count = 0; diff --git a/src/fishing.nvgt b/src/fishing.nvgt index 6d6291f..47bf267 100644 --- a/src/fishing.nvgt +++ b/src/fishing.nvgt @@ -105,7 +105,7 @@ int get_random_stream_tile() { } string get_random_fish_type() { - bool is_night = (current_hour >= 18 || current_hour < 7); + bool is_night = is_night_hour(current_hour); if (is_night) { int roll = random(0, 99); diff --git a/src/goals.nvgt b/src/goals.nvgt index 73a16f2..f0d85c5 100644 --- a/src/goals.nvgt +++ b/src/goals.nvgt @@ -484,6 +484,10 @@ int get_resident_food_interval_hours() { return 8; } +bool leadership_mastery_allows_night_work() { + return leadershipMasteryLevel >= 4; +} + bool taming_mastery_allows_retrieval() { return tamingMasteryLevel >= 1; } diff --git a/src/save_system.nvgt b/src/save_system.nvgt index b5b566b..7b18c66 100644 --- a/src/save_system.nvgt +++ b/src/save_system.nvgt @@ -1638,7 +1638,7 @@ bool load_game_state_from_file(const string& in filename) { if (current_day < 1) current_day = 1; - is_daytime = (current_hour >= 6 && current_hour < 19); + is_daytime = is_day_hour_for_day(current_hour, current_day); hour_timer.restart(); string weather_data; diff --git a/src/time_system.nvgt b/src/time_system.nvgt index 79a66a6..1e632fe 100644 --- a/src/time_system.nvgt +++ b/src/time_system.nvgt @@ -1,6 +1,9 @@ // Time System // 1 real minute = 1 in-game hour const int MS_PER_HOUR = 60000; +const int DAY_START_HOUR = 6; +const int NIGHT_START_BASE_HOUR = 19; +const int NIGHT_START_SHIFT_DAYS = 9; int current_hour = 8; // Start at 8 AM int current_day = 1; // Track current day @@ -361,6 +364,24 @@ void start_invasion() { notify(trf("system.time.event.invasion_start", args)); } +int get_undead_resident_raid_count() { + int count = UNDEAD_RESIDENT_RAID_BASE_COUNT + (current_day / UNDEAD_RESIDENT_RAID_DAYS_PER_EXTRA); + if (count > UNDEAD_RESIDENT_RAID_MAX_COUNT) + count = UNDEAD_RESIDENT_RAID_MAX_COUNT; + return count; +} + +void start_undead_resident_raid() { + int count = get_undead_resident_raid_count(); + for (int i = 0; i < count; i++) { + spawn_undead("undead_resident", false); + } + + dictionary args; + args.set("count", count); + notify(trf("system.time.event.undead_resident_raid", args)); +} + void update_invasion_chance_for_new_day() { if (current_day == 2) { invasion_chance = 100; @@ -385,17 +406,52 @@ int get_random_invasion_hour(int min_hour) { return random(min_hour, 11); } +int get_night_start_hour_for_day(int day) { + if (day < 1) + day = 1; + int nightStartHour = NIGHT_START_BASE_HOUR - (day / NIGHT_START_SHIFT_DAYS); + if (nightStartHour < DAY_START_HOUR) + nightStartHour = DAY_START_HOUR; + return nightStartHour; +} + +int get_current_night_start_hour() { + return get_night_start_hour_for_day(current_day); +} + +bool is_night_hour_for_day(int hour, int day) { + int nightStartHour = get_night_start_hour_for_day(day); + return hour < DAY_START_HOUR || hour >= nightStartHour; +} + bool is_night_hour(int hour) { - return hour < 6 || hour >= 19; + return is_night_hour_for_day(hour, current_day); +} + +bool is_day_hour_for_day(int hour, int day) { + return !is_night_hour_for_day(hour, day); +} + +bool is_day_hour(int hour) { + return !is_night_hour(hour); +} + +bool has_daylight_for_day(int day) { + return get_night_start_hour_for_day(day) > DAY_START_HOUR; +} + +bool has_daylight_today() { + return has_daylight_for_day(current_day); } int get_random_zombie_swarm_hour(int min_hour) { if (!is_night_hour(min_hour)) return -1; - if (min_hour >= 19) { + int nightStartHour = get_current_night_start_hour(); + if (min_hour >= nightStartHour) { return random(min_hour, 23); } - return random(min_hour, 5); + return random(min_hour, DAY_START_HOUR - 1); } void schedule_invasion() { @@ -416,7 +472,11 @@ void check_scheduled_invasion() { if (current_hour == invasion_scheduled_hour) { invasion_scheduled_hour = -1; invasion_triggered_today = true; - start_invasion(); + if (is_daytime) { + start_invasion(); + } else { + start_undead_resident_raid(); + } } else if (current_hour > 11) { invasion_scheduled_hour = -1; } @@ -849,16 +909,18 @@ void update_time() { } handle_pet_hourly_update(current_hour); - if (current_hour == 18 && !sun_setting_warned) { + int nightStartHour = get_current_night_start_hour(); + int sunsetWarningHour = nightStartHour - 1; + if (has_daylight_today() && current_hour == sunsetWarningHour && !sun_setting_warned) { notify(tr("system.time.event.sun_setting")); sun_setting_warned = true; - } else if (current_hour == 19) { + } else if (current_hour == nightStartHour) { sun_setting_warned = false; } - if (current_hour == 5 && !sunrise_warned) { + if (has_daylight_today() && current_hour == DAY_START_HOUR - 1 && !sunrise_warned) { notify(tr("system.time.event.sky_brightening")); sunrise_warned = true; - } else if (current_hour == 6) { + } else if (current_hour == DAY_START_HOUR) { sunrise_warned = false; } @@ -869,7 +931,7 @@ void update_time() { 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); + bool expected_daytime = is_day_hour(current_hour); if (expected_daytime != is_daytime) { is_daytime = expected_daytime; update_ambience(true); @@ -877,7 +939,7 @@ void update_time() { } if (is_daytime && residents_count > 0 && barricade_health < BARRICADE_MAX_HEALTH) { - const int day_start_hour = 6; + const int day_start_hour = DAY_START_HOUR; 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; @@ -898,7 +960,7 @@ void update_time() { } } - if (current_hour == 6) { + if (current_hour == DAY_START_HOUR) { if (undead_residents_pending > 0) { undead_residents_count += undead_residents_pending; undead_residents_pending = 0; @@ -930,7 +992,7 @@ void update_time() { attempt_resident_fishing(); attempt_resident_fish_smoking(); attempt_livestock_production(); - if (current_hour == 6) { + if (current_hour == DAY_START_HOUR) { save_game_state(); } } @@ -1021,16 +1083,16 @@ string get_time_string() { } 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) + int nightStartHour = get_current_night_start_hour(); + if (!has_daylight_today()) + return; - // Start crossfade to night at hour 18 - if (current_hour == 18 && is_daytime && !crossfade_active) { + // Start crossfade to night one hour before the current day's night boundary. + if (current_hour == nightStartHour - 1 && 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) { + else if (current_hour == DAY_START_HOUR - 1 && !is_daytime && !crossfade_active) { start_crossfade(false); // Fade to day } }