diff --git a/files/instructions.md b/files/instructions.md index 95ee9d0..e518b97 100644 --- a/files/instructions.md +++ b/files/instructions.md @@ -134,25 +134,31 @@ Pets are optional companions that can fight and retrieve drops ### Calling and status - **Space** call your pet -- If your pet is out Space calls it back +- If your pet is out Space recalls it and your pet runs back to you - If your pet is with you Space sends it out and costs 1 loyalty +- If loyalty is 2 or less your pet will not go out and says it is hungry and unresponsive - If your pet is knocked out Space reports remaining recovery time - Press H to hear pet health and loyalty when you have a pet ### Loyalty and feeding - Loyalty starts at 5 and max is 10 -- If no food is available loyalty drops by 2 each hour -- Every 8 hours your pet eats 1 food and gains 2 loyalty if food is available +- While your pet is out loyalty drops by 1 each hour +- At loyalty 1 you get a hungry warning notification +- If loyalty reaches 0 your pet leaves +- Every 8 hours your pet can eat 1 food only while it is not out +- Eating restores loyalty to max - Food can be meat smoked fish raw fish or basket of fruits and nuts - Food is taken from your inventory first then storage -- If loyalty reaches 0 your pet leaves ### Help in the field - When out your pet attacks bandits undead and boars within range - Each successful hit costs the pet 1 health +- Pets do not heal while out - If health reaches 0 the pet is knocked out for 3 hours and then recovers - When out your pet can retrieve world drops like small game boar carcasses and arrows +- Retrieval messages are spoken without the pet sound cue - If you cannot carry the item the pet keeps it and it is lost +- Pets stay out and follow you until you recall them with Space or they leave from loyalty loss ### Random finds - If loyalty is at least 5 your pet can bring back sticks vines stones or clay diff --git a/sounds/pets/black_cat.ogg b/sounds/pets/black_cat.ogg new file mode 100644 index 0000000..e5ae38d --- /dev/null +++ b/sounds/pets/black_cat.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:47b514670264ce2710e83088b351e10e29dda02ade4de67357a15944ec664d42 +size 75631 diff --git a/sounds/terrain/fly.ogg b/sounds/terrain/fly.ogg new file mode 100644 index 0000000..c199a5a --- /dev/null +++ b/sounds/terrain/fly.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e0ce93a20815637a15f117e0e03768436c146f9ddf9a61b6239f20c67147a035 +size 4542 diff --git a/src/base_system.nvgt b/src/base_system.nvgt index 74e2927..2f8a4c0 100644 --- a/src/base_system.nvgt +++ b/src/base_system.nvgt @@ -44,13 +44,15 @@ int get_horse_success_bonus() { return count * HORSE_SUCCESS_BONUS_PER; } -int get_horse_damage_bonus() { +int get_horse_resident_cooldown_reduction() { if (world_stables.length() == 0) return 0; if (world_storages.length() == 0) return 0; if (horses_count <= 0) return 0; int count = horses_count; if (count > MAX_HORSES) count = MAX_HORSES; - return count / HORSE_DAMAGE_BONUS_STEP; + int cooldown_range = RESIDENT_COMBAT_BASE_COOLDOWN - RESIDENT_COMBAT_TARGET_COOLDOWN; + if (cooldown_range <= 0) return 0; + return (cooldown_range * count) / MAX_HORSES; } int get_resident_success_chance(int base_chance) { @@ -80,12 +82,19 @@ int get_resident_cooldown(int base_cooldown) { return cooldown; } +int get_resident_combat_cooldown() { + int base_cooldown = RESIDENT_COMBAT_BASE_COOLDOWN - get_horse_resident_cooldown_reduction(); + if (base_cooldown < RESIDENT_COMBAT_TARGET_COOLDOWN) { + base_cooldown = RESIDENT_COMBAT_TARGET_COOLDOWN; + } + return get_resident_cooldown(base_cooldown); +} + int apply_resident_damage_bonus(int damage) { int adjusted = damage; if (blessing_resident_active) { adjusted *= get_resident_effect_multiplier(); } - adjusted += get_horse_damage_bonus(); return adjusted; } @@ -258,22 +267,43 @@ void attempt_livestock_production() { void keep_base_fires_fed() { if (residents_count <= 0) return; - if (!has_any_storage_food()) return; - if (get_storage_count(ITEM_STICKS) <= 0 && get_storage_count(ITEM_LOGS) <= 0) return; + if (get_storage_count(ITEM_VINES) <= 0 && get_storage_count(ITEM_STICKS) <= 0 && get_storage_count(ITEM_LOGS) <= 0) return; + + // Residents tend fires once per in-game hour from time_system. + // Keep a 1-hour buffer above the 24-hour floor so fuel does not dip below 24 between hourly checks. + const int fire_floor_ms = 24 * 60000; + const int fire_target_ms = fire_floor_ms + 60000; + const int vine_fuel_ms = 60000; // 1 hour + const int stick_fuel_ms = 300000; // 5 hours + const int log_fuel_ms = 720000; // 12 hours for (uint i = 0; i < world_fires.length(); i++) { if (world_fires[i].position > BASE_END) continue; if (!world_fires[i].is_burning()) continue; - if (world_fires[i].fuel_remaining > 300000) continue; + if (world_fires[i].fuel_remaining >= fire_target_ms) continue; - if (get_storage_count(ITEM_STICKS) > 0) { - add_storage_count(ITEM_STICKS, -1); - world_fires[i].add_fuel(300000); // 5 minutes - } else if (get_storage_count(ITEM_LOGS) > 0) { - add_storage_count(ITEM_LOGS, -1); - world_fires[i].add_fuel(720000); // 12 minutes + while (world_fires[i].fuel_remaining < fire_target_ms) { + int needed = fire_target_ms - world_fires[i].fuel_remaining; + + if (needed >= log_fuel_ms && get_storage_count(ITEM_LOGS) > 0) { + add_storage_count(ITEM_LOGS, -1); + world_fires[i].add_fuel(log_fuel_ms); + } else if (needed >= stick_fuel_ms && get_storage_count(ITEM_STICKS) > 0) { + add_storage_count(ITEM_STICKS, -1); + world_fires[i].add_fuel(stick_fuel_ms); + } else if (get_storage_count(ITEM_VINES) > 0) { + add_storage_count(ITEM_VINES, -1); + world_fires[i].add_fuel(vine_fuel_ms); + } else if (get_storage_count(ITEM_STICKS) > 0) { + add_storage_count(ITEM_STICKS, -1); + world_fires[i].add_fuel(stick_fuel_ms); + } else if (get_storage_count(ITEM_LOGS) > 0) { + add_storage_count(ITEM_LOGS, -1); + world_fires[i].add_fuel(log_fuel_ms); + } else { + break; + } } - break; } } @@ -374,8 +404,11 @@ int choose_defense_weapon_type() { return (roll <= spearCount) ? RESIDENT_WEAPON_SPEAR : RESIDENT_WEAPON_SLING; } -int perform_resident_defense() { +timer resident_combat_timer; + +int perform_resident_defense(int target_pos) { if (!can_residents_defend()) return 0; + if (resident_combat_timer.elapsed < get_resident_combat_cooldown()) return 0; // Choose weapon type (bows preferred, otherwise weighted by availability) int weapon_type = choose_defense_weapon_type(); @@ -389,9 +422,12 @@ int perform_resident_defense() { int damage = 0; if (weapon_type == RESIDENT_WEAPON_BOW && bowCount > 0) { - damage = apply_resident_damage_bonus(random(RESIDENT_SLING_DAMAGE_MIN, RESIDENT_SLING_DAMAGE_MAX)); + damage = apply_resident_damage_bonus(random(RESIDENT_BOW_DAMAGE_MIN, RESIDENT_BOW_DAMAGE_MAX)); add_storage_count(ITEM_ARROWS, -1); play_1d_with_volume_step("sounds/weapons/bow_fire.ogg", x, BASE_END + 1, false, RESIDENT_DEFENSE_VOLUME_STEP); + if (target_pos >= 0 && random(1, 100) <= 25 && get_drop_at(target_pos) == null) { + add_world_drop(target_pos, "arrow"); + } } else if (weapon_type == RESIDENT_WEAPON_SPEAR && spearCount > 0) { damage = apply_resident_damage_bonus(RESIDENT_SPEAR_DAMAGE); // Weapons don't get consumed on use - they break via daily breakage check @@ -404,12 +440,14 @@ int perform_resident_defense() { play_1d_with_volume_step("sounds/weapons/sling_hit.ogg", x, BASE_END + 1, false, RESIDENT_DEFENSE_VOLUME_STEP); } + if (damage > 0) { + resident_combat_timer.restart(); + } + return damage; } // Proactive resident ranged defense -timer resident_ranged_timer; - void attempt_resident_ranged_defense() { // Only if residents exist and have ranged weapons if (residents_count <= 0) return; @@ -419,8 +457,8 @@ void attempt_resident_ranged_defense() { bool has_sling = (slingCount > 0 && get_storage_count(ITEM_STONES) > 0); if (!has_bow && !has_sling) return; - // Cooldown between shots - if (resident_ranged_timer.elapsed < get_resident_cooldown(RESIDENT_SLING_COOLDOWN)) return; + // Shared cooldown for all resident combat actions + if (resident_combat_timer.elapsed < get_resident_combat_cooldown()) return; int range = has_bow ? BOW_RANGE : SLING_RANGE; // Find nearest enemy within range @@ -454,13 +492,18 @@ void attempt_resident_ranged_defense() { if (targetPos == -1) return; // Shoot! - resident_ranged_timer.restart(); - int damage = apply_resident_damage_bonus(random(RESIDENT_SLING_DAMAGE_MIN, RESIDENT_SLING_DAMAGE_MAX)); + resident_combat_timer.restart(); + int damage = 0; if (has_bow) { + damage = apply_resident_damage_bonus(random(RESIDENT_BOW_DAMAGE_MIN, RESIDENT_BOW_DAMAGE_MAX)); add_storage_count(ITEM_ARROWS, -1); play_1d_with_volume_step("sounds/weapons/bow_fire.ogg", x, BASE_END + 1, false, RESIDENT_DEFENSE_VOLUME_STEP); play_1d_with_volume_step("sounds/weapons/arrow_hit.ogg", x, targetPos, false, RESIDENT_DEFENSE_VOLUME_STEP); + if (random(1, 100) <= 25 && get_drop_at(targetPos) == null) { + add_world_drop(targetPos, "arrow"); + } } else { + damage = apply_resident_damage_bonus(random(RESIDENT_SLING_DAMAGE_MIN, RESIDENT_SLING_DAMAGE_MAX)); add_storage_count(ITEM_STONES, -1); play_1d_with_volume_step("sounds/weapons/sling_hit.ogg", x, targetPos, false, RESIDENT_DEFENSE_VOLUME_STEP); } diff --git a/src/constants.nvgt b/src/constants.nvgt index f084f3b..28ab388 100644 --- a/src/constants.nvgt +++ b/src/constants.nvgt @@ -29,8 +29,11 @@ const int BLESSING_HEAL_AMOUNT = 3; const int BLESSING_BARRICADE_REPAIR = 20; const int BLESSING_SPEED_DURATION = 300000; const int BLESSING_RESIDENT_DURATION = 300000; +const int BLESSING_SEARCH_DURATION = 300000; const int BLESSING_TRIGGER_CHANCE = 10; const int BLESSING_WALK_SPEED = 320; +const int BLESSING_SEARCH_GATHER_BONUS = 30; +const int GATHER_TIME_REDUCTION_CAP = 75; const int FISH_WEIGHT_MIN = 1; const int FISH_WEIGHT_MAX = 30; // Player sex constants @@ -153,7 +156,6 @@ const int MAX_LIVESTOCK = 9; const int HORSE_ADVENTURE_CHANCE = 45; const int LIVESTOCK_ADVENTURE_CHANCE = 45; const int HORSE_SUCCESS_BONUS_PER = 2; -const int HORSE_DAMAGE_BONUS_STEP = 3; const int LIVESTOCK_MEAT_CHANCE = 6; const int LIVESTOCK_SKIN_CHANCE = 3; const int LIVESTOCK_FEATHER_CHANCE = 2; @@ -238,6 +240,10 @@ const int RESIDENT_WEAPON_BREAK_CHANCE = 10; const int RESIDENT_SPEAR_DAMAGE = 2; const int RESIDENT_SLING_DAMAGE_MIN = 3; const int RESIDENT_SLING_DAMAGE_MAX = 5; +const int RESIDENT_BOW_DAMAGE_MIN = 4; +const int RESIDENT_BOW_DAMAGE_MAX = 6; +const int RESIDENT_COMBAT_BASE_COOLDOWN = 3200; // Base attack delay (2x player axe) +const int RESIDENT_COMBAT_TARGET_COOLDOWN = 1600; // At max horses, about player axe speed const int RESIDENT_SNARE_ESCAPE_CHANCE = 5; // 5% chance game escapes when resident retrieves const int RESIDENT_SNARE_CHECK_CHANCE = 15; // 15% chance per hour to check snares const int RESIDENT_FISHING_CHANCE = 6; // 6% chance per resident per hour to catch a fish @@ -348,7 +354,6 @@ const int FALL_DAMAGE_MIN = 0; const int FALL_DAMAGE_MAX = 4; // Base Automation -const int RESIDENT_SLING_COOLDOWN = 4000; // 4 seconds between shots const int RESIDENT_COLLECTION_CHANCE = 10; // 10% chance per basket per hour const int RESIDENT_FORAGING_CHANCE = 50; // 50% chance per resident per attempt (daily) diff --git a/src/enemies/bandit.nvgt b/src/enemies/bandit.nvgt index e3f9912..7401a15 100644 --- a/src/enemies/bandit.nvgt +++ b/src/enemies/bandit.nvgt @@ -184,6 +184,54 @@ int pick_bandit_spawn_east_of_player(int min_distance, int max_distance, int ran return pick_bandit_spawn_position(start, end); } +int pick_bandit_spawn_west_of_player(int min_distance, int max_distance, int range_start, int range_end) { + int min_dist = min_distance; + int max_dist = max_distance; + if (min_dist < 0) min_dist = 0; + if (max_dist < min_dist) { + int temp = min_dist; + min_dist = max_dist; + max_dist = temp; + } + + int start = x - max_dist; + int end = x - min_dist; + + int range_start_norm = range_start; + int range_end_norm = range_end; + if (range_start_norm > range_end_norm) { + int temp = range_start_norm; + range_start_norm = range_end_norm; + range_end_norm = temp; + } + + if (start < range_start_norm) start = range_start_norm; + if (end > range_end_norm) end = range_end_norm; + + if (start > end) return -1; + + return pick_bandit_spawn_position(start, end); +} + +int pick_bandit_spawn_near_player(int range_start, int range_end) { + // Preferred: 30-50 tiles east of player. + int spawn_x = pick_bandit_spawn_east_of_player(BANDIT_SPAWN_MIN_DISTANCE, BANDIT_SPAWN_MAX_DISTANCE, range_start, range_end); + if (spawn_x != -1) return spawn_x; + + // Fallback: 30-50 tiles west when east side is not available. + spawn_x = pick_bandit_spawn_west_of_player(BANDIT_SPAWN_MIN_DISTANCE, BANDIT_SPAWN_MAX_DISTANCE, range_start, range_end); + if (spawn_x != -1) return spawn_x; + + // If map bounds are tight, relax minimum distance but keep around player. + spawn_x = pick_bandit_spawn_east_of_player(1, BANDIT_SPAWN_MAX_DISTANCE, range_start, range_end); + if (spawn_x != -1) return spawn_x; + + spawn_x = pick_bandit_spawn_west_of_player(1, BANDIT_SPAWN_MAX_DISTANCE, range_start, range_end); + if (spawn_x != -1) return spawn_x; + + return -1; +} + int count_bandits_in_range(int range_start, int range_end) { int start = range_start; int end = range_end; @@ -205,7 +253,7 @@ int count_bandits_in_range(int range_start, int range_end) { void spawn_bandit(int expansion_start, int expansion_end, const string&in invader_type = "bandit") { int spawn_x = -1; if (invasion_active) { - spawn_x = pick_bandit_spawn_east_of_player(BANDIT_SPAWN_MIN_DISTANCE, BANDIT_SPAWN_MAX_DISTANCE, expansion_start, expansion_end); + spawn_x = pick_bandit_spawn_near_player(expansion_start, expansion_end); } if (spawn_x == -1) { spawn_x = pick_bandit_spawn_position(expansion_start, expansion_end); @@ -309,7 +357,7 @@ void try_attack_barricade_bandit(Bandit@ bandit) { // Resident defense counter-attack if (can_residents_defend()) { - int counterDamage = perform_resident_defense(); + int counterDamage = perform_resident_defense(bandit.position); if (counterDamage > 0) { int before_health = bandit.health; damage_bandit_at(bandit.position, counterDamage); diff --git a/src/enemies/undead.nvgt b/src/enemies/undead.nvgt index 34d3d91..fdc1669 100644 --- a/src/enemies/undead.nvgt +++ b/src/enemies/undead.nvgt @@ -269,7 +269,7 @@ void try_attack_barricade_undead(Undead@ undead) { // Resident defense counter-attack if (can_residents_defend()) { - int counterDamage = perform_resident_defense(); + int counterDamage = perform_resident_defense(undead.position); if (counterDamage > 0) { int before_health = undead.health; damage_undead_at(undead.position, counterDamage); diff --git a/src/pet_system.nvgt b/src/pet_system.nvgt index d3bbb6a..8f713b5 100644 --- a/src/pet_system.nvgt +++ b/src/pet_system.nvgt @@ -6,7 +6,9 @@ string petSoundPath = ""; string petType = ""; string petGender = ""; int petLoyalty = 0; +bool petHungryWarned = false; bool petOut = false; +bool petRecallRequested = false; int petPosition = 0; bool petPositionValid = false; int petHealth = 0; @@ -23,6 +25,7 @@ int petEventSoundHandle = -1; const int PET_TRAVEL_NONE = 0; const int PET_TRAVEL_ATTACK = 1; const int PET_TRAVEL_RETRIEVE = 2; +const int PET_TRAVEL_RETURN = 3; bool petTravelActive = false; int petTravelAction = PET_TRAVEL_NONE; @@ -193,11 +196,15 @@ bool consume_pet_food() { return false; } -void queue_pet_event(const string&in message, int soundPos = -1) { +void queue_pet_event(const string&in message, int soundPos = -1, bool playSound = true) { if (!petActive) return; if (message.length() == 0) return; petEventMessages.insert_last(message); - petEventSounds.insert_last(petSoundPath); + if (playSound) { + petEventSounds.insert_last(petSoundPath); + } else { + petEventSounds.insert_last(""); + } petEventPositions.insert_last(soundPos); } @@ -226,6 +233,44 @@ int get_pet_search_origin() { return get_pet_listener_pos(); } +bool is_hawk_pet() { + return normalize_pet_name_text(petType).lower() == "hawk"; +} + +void play_pet_travel_step_sound(int stepPos) { + int listenerPos = get_pet_listener_pos(); + if (abs(stepPos - listenerPos) > CREATURE_DEFAULT_FOOTSTEP_DISTANCE) return; + + if (is_hawk_pet()) { + string flySoundPath = "sounds/terrain/fly.ogg"; + if (file_exists(flySoundPath)) { + play_1d_with_volume_step(flySoundPath, listenerPos, stepPos, false, CREATURE_DEFAULT_VOLUME_STEP); + return; + } + } + + play_creature_footstep(listenerPos, stepPos, BASE_END, GRASS_END); +} + +void update_pet_travel_position(int currentPos) { + int previousPos = petPositionValid ? petPosition : petTravelStartPos; + if (!petPositionValid) { + petPosition = petTravelStartPos; + petPositionValid = true; + previousPos = petPosition; + } + if (currentPos == previousPos) return; + + int stepDir = (currentPos > previousPos) ? 1 : -1; + int stepPos = previousPos + stepDir; + while (true) { + play_pet_travel_step_sound(stepPos); + if (stepPos == currentPos) break; + stepPos += stepDir; + } + petPosition = currentPos; +} + bool is_pet_knocked_out() { return petKnockoutHoursRemaining > 0 || petHealth <= 0; } @@ -274,6 +319,18 @@ void queue_pet_return_event() { queue_pet_event("A " + get_pet_display_name() + " returns to you."); } +bool is_pet_at_player_position() { + return petPositionValid && petPosition == get_pet_listener_pos(); +} + +void complete_pet_return() { + petOut = false; + petRecallRequested = false; + petPositionValid = false; + queue_pet_return_event(); + stop_pet_travel(); +} + void knock_out_pet() { if (!petActive) return; petHealth = 0; @@ -329,7 +386,9 @@ void reset_pet_state() { petType = ""; petGender = ""; petLoyalty = 0; + petHungryWarned = false; petOut = false; + petRecallRequested = false; petPosition = 0; petPositionValid = false; petHealth = PET_HEALTH_MAX; @@ -393,21 +452,26 @@ void check_pet_call_key() { return; } if (petOut) { - petOut = false; - stop_pet_travel(); - petPositionValid = false; - queue_pet_return_event(); + petRecallRequested = true; + play_pet_recall_ack(); + speak_with_history("Your pet is on its way.", true); + request_pet_return(); + return; + } + if (petLoyalty <= 2) { + speak_with_history("A " + get_pet_display_name() + " is hungry and unresponsive.", true); return; } adjust_pet_loyalty(-PET_LOYALTY_CALLOUT_COST); if (!petActive) return; petOut = true; + petRecallRequested = false; petPosition = get_pet_listener_pos(); petPositionValid = true; - if (file_exists("sounds/action/call_pet.ogg")) { + if (file_exists("sounds/actions/call_pet.ogg")) { /* But I can feel it, black water washes over me. As it soothes I call to you with my control. */ - p.play_stationary("sounds/action/call_pet.ogg", false); + p.play_stationary("sounds/actions/call_pet.ogg", false); } } @@ -417,7 +481,9 @@ void adopt_pet(const string&in soundPath) { petType = get_pet_name_from_sound_path(soundPath); petGender = (random(0, 1) == 0) ? "Male" : "Female"; petLoyalty = PET_START_LOYALTY; + petHungryWarned = false; petOut = false; + petRecallRequested = false; petPosition = 0; petPositionValid = false; petHealth = PET_HEALTH_MAX; @@ -452,6 +518,8 @@ void start_pet_travel_attack(int targetPos, const string&in targetLabel, int tar petTravelActive = true; petTravelAction = PET_TRAVEL_ATTACK; petTravelStartPos = petPositionValid ? petPosition : get_pet_listener_pos(); + petPosition = petTravelStartPos; + petPositionValid = true; petTravelTargetPos = targetPos; petTravelTargetLabel = targetLabel; petTravelTargetKind = targetKind; @@ -474,11 +542,63 @@ void start_pet_travel_retrieve(int targetPos) { petTravelActive = true; petTravelAction = PET_TRAVEL_RETRIEVE; petTravelStartPos = petPositionValid ? petPosition : get_pet_listener_pos(); + petPosition = petTravelStartPos; + petPositionValid = true; petTravelTargetPos = targetPos; petTravelDurationMs = get_pet_travel_duration_ms(petTravelStartPos, targetPos); petTravelTimer.restart(); } +void start_pet_travel_return(int targetPos) { + stop_pet_travel(); + petTravelActive = true; + petTravelAction = PET_TRAVEL_RETURN; + petTravelStartPos = petPositionValid ? petPosition : get_pet_listener_pos(); + petPosition = petTravelStartPos; + petPositionValid = true; + petTravelTargetPos = targetPos; + petTravelDurationMs = get_pet_travel_duration_ms(petTravelStartPos, targetPos); + petTravelTimer.restart(); +} + +void request_pet_return() { + if (!petActive || !petOut) return; + if (!petPositionValid) { + petPosition = get_pet_listener_pos(); + petPositionValid = true; + } + if (is_pet_at_player_position()) { + complete_pet_return(); + return; + } + start_pet_travel_return(get_pet_listener_pos()); +} + +void request_pet_follow() { + if (!petActive || !petOut) return; + petRecallRequested = false; + if (!petPositionValid) { + petPosition = get_pet_listener_pos(); + petPositionValid = true; + } + if (is_pet_at_player_position()) { + stop_pet_travel(); + return; + } + start_pet_travel_return(get_pet_listener_pos()); +} + +void play_pet_recall_ack() { + if (!petActive) return; + if (petSoundPath == "" || !file_exists(petSoundPath)) return; + int listenerPos = get_pet_listener_pos(); + if (petPositionValid) { + play_1d_with_volume_step(petSoundPath, listenerPos, petPosition, false, PLAYER_WEAPON_SOUND_VOLUME_STEP); + } else { + p.play_stationary(petSoundPath, false); + } +} + void update_pet_travel() { if (!petTravelActive) return; if (petTravelDurationMs < 1) petTravelDurationMs = 1; @@ -488,6 +608,8 @@ void update_pet_travel() { if (find_pet_attack_target_by_kind(petTravelTargetKind, petTravelTargetPos, refreshedTargetPos)) { petTravelTargetPos = refreshedTargetPos; } + } else if (petTravelAction == PET_TRAVEL_RETURN) { + petTravelTargetPos = get_pet_listener_pos(); } int elapsed = petTravelTimer.elapsed; @@ -496,6 +618,7 @@ void update_pet_travel() { int travel = int(float(petTravelTargetPos - petTravelStartPos) * progress); int currentPos = petTravelStartPos + travel; + update_pet_travel_position(currentPos); if (petTravelSoundHandle != -1) { p.update_sound_1d(petTravelSoundHandle, currentPos); } @@ -539,9 +662,7 @@ void update_pet_travel() { start_pet_travel_retrieve(drop.position); return; } - petOut = false; - petPositionValid = false; - queue_pet_return_event(); + request_pet_follow(); } } else if (petTravelAction == PET_TRAVEL_RETRIEVE) { WorldDrop@ drop = get_drop_at(petTravelTargetPos); @@ -549,7 +670,7 @@ void update_pet_travel() { string message = ""; if (try_pet_pickup_world_drop(drop, message)) { remove_drop_at(drop.position); - queue_pet_event(message); + queue_pet_event(message, -1, false); petPosition = petTravelTargetPos; petPositionValid = true; WorldDrop@ nextDrop = find_pet_drop_target(); @@ -566,11 +687,22 @@ void update_pet_travel() { start_pet_travel_attack(nextTargetPos, nextTargetLabel, nextTargetKind); return; } - petOut = false; - petPositionValid = false; - queue_pet_return_event(); + request_pet_follow(); } } + } else if (petTravelAction == PET_TRAVEL_RETURN) { + petPosition = petTravelTargetPos; + petPositionValid = true; + if (is_pet_at_player_position()) { + if (petRecallRequested) { + complete_pet_return(); + } else { + stop_pet_travel(); + } + return; + } + start_pet_travel_return(get_pet_listener_pos()); + return; } stop_pet_travel(); @@ -840,6 +972,25 @@ void update_pet_attack() { start_pet_travel_attack(targetPos, targetLabel, targetKind); } +void update_pet_follow() { + if (!petActive) return; + if (!petOut) return; + if (petAdventureMode) return; + if (is_pet_knocked_out()) return; + if (petTravelActive) return; + + WorldDrop@ drop = find_pet_drop_target(); + if (drop !is null) return; + + int targetPos = -1; + string targetLabel = ""; + int targetKind = -1; + if (find_pet_attack_target(targetPos, targetLabel, targetKind)) return; + + if (is_pet_at_player_position()) return; + request_pet_follow(); +} + void attempt_pet_random_find() { if (!petActive) return; if (!petOut) return; @@ -853,9 +1004,8 @@ void attempt_pet_random_find() { add_personal_count(itemType, added); string itemName = (added == 1) ? item_registry[itemType].singular : item_registry[itemType].name; - queue_pet_event("Your " + get_pet_display_name() + " retrieved " + added + " " + itemName + ". A " + get_pet_display_name() + " returns to you.", x); - petOut = false; - petPositionValid = false; + queue_pet_event("Your " + get_pet_display_name() + " retrieved " + added + " " + itemName + ".", petPositionValid ? petPosition : x, false); + request_pet_follow(); } void handle_pet_hourly_update(int hour) { @@ -870,21 +1020,26 @@ void handle_pet_hourly_update(int hour) { } } - if (petKnockoutHoursRemaining <= 0 && petHealth > 0 && petHealth < PET_HEALTH_MAX) { + if (petKnockoutHoursRemaining <= 0 && !petOut && petHealth > 0 && petHealth < PET_HEALTH_MAX) { petHealth += 1; if (petHealth > PET_HEALTH_MAX) petHealth = PET_HEALTH_MAX; } - if (!has_pet_food_available()) { - adjust_pet_loyalty(-PET_LOYALTY_HUNGER_LOSS); + if (petOut) { + adjust_pet_loyalty(-1); + if (!petActive) return; + if (petLoyalty <= 1 && !petHungryWarned) { + notify("A " + get_pet_display_name() + " is getting hungry."); + petHungryWarned = true; + } } - if (!petActive) return; - - if (hour % 8 == 0) { + if (hour % 8 == 0 && !petOut) { if (consume_pet_food()) { - petLoyalty += PET_LOYALTY_EAT_BONUS; - clamp_pet_loyalty(); + petLoyalty = PET_LOYALTY_MAX; + if (petLoyalty > 1) { + petHungryWarned = false; + } } } @@ -896,6 +1051,7 @@ void update_pets() { if (petActive && petOut) { update_pet_retrieval(); update_pet_attack(); + update_pet_follow(); } update_pet_events(); } diff --git a/src/player.nvgt b/src/player.nvgt index cc1c0c0..1a61547 100644 --- a/src/player.nvgt +++ b/src/player.nvgt @@ -80,6 +80,8 @@ bool blessing_speed_active = false; timer blessing_speed_timer; bool blessing_resident_active = false; timer blessing_resident_timer; +bool blessing_search_active = false; +timer blessing_search_timer; // Timers timer walktimer; diff --git a/src/runes/rune_effects.nvgt b/src/runes/rune_effects.nvgt index 4033d53..d31b84f 100644 --- a/src/runes/rune_effects.nvgt +++ b/src/runes/rune_effects.nvgt @@ -36,14 +36,17 @@ int get_total_rune_gather_bonus() { return bonus; } -// Apply gathering time reduction based on rune bonuses +// Apply gathering time reduction based on runes and blessings // Takes base time in ms, returns reduced time int apply_rune_gather_bonus(int base_time) { int bonus_percent = get_total_rune_gather_bonus(); + if (blessing_search_active) { + bonus_percent += BLESSING_SEARCH_GATHER_BONUS; + } if (bonus_percent <= 0) return base_time; - // Cap at 50% reduction to prevent instant gathering - if (bonus_percent > 50) bonus_percent = 50; + // Keep gathering from becoming instant. + if (bonus_percent > GATHER_TIME_REDUCTION_CAP) bonus_percent = GATHER_TIME_REDUCTION_CAP; int reduction = (base_time * bonus_percent) / 100; return base_time - reduction; diff --git a/src/save_system.nvgt b/src/save_system.nvgt index 142553f..858865d 100644 --- a/src/save_system.nvgt +++ b/src/save_system.nvgt @@ -623,6 +623,7 @@ void reset_game_state() { incense_burning = false; blessing_speed_active = false; blessing_resident_active = false; + blessing_search_active = false; reset_fylgja_state(); reset_pet_state(); diff --git a/src/time_system.nvgt b/src/time_system.nvgt index 5b1755f..a56ccbf 100644 --- a/src/time_system.nvgt +++ b/src/time_system.nvgt @@ -662,6 +662,10 @@ void update_blessings() { blessing_resident_active = false; speak_with_history("The residents' purpose fades.", true); } + if (blessing_search_active && blessing_search_timer.elapsed >= BLESSING_SEARCH_DURATION) { + blessing_search_active = false; + speak_with_history("The eagle's sight fades.", true); + } } void attempt_blessing() { @@ -674,6 +678,7 @@ void attempt_blessing() { 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 (!blessing_search_active) options.insert_last(4); if (options.length() == 0) return; int choice = options[random(0, options.length() - 1)]; @@ -708,6 +713,10 @@ void attempt_blessing() { blessing_resident_active = true; blessing_resident_timer.restart(); notify(god_name + " radiance fills residents with purpose."); + } else if (choice == 4) { + blessing_search_active = true; + blessing_search_timer.restart(); + notify(god_name + " favor grants you the eyes of an eagle."); } }