From 6687863fbefd4d99442d82e4ecd6b2da3b5f48c9 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Thu, 12 Feb 2026 18:23:12 -0500 Subject: [PATCH] Pets a lot nicer now, hopefully most of the bugs fixed. --- draugnorak.nvgt | 2 +- files/credits.md | 8 +- src/bosses/bandit_hideout.nvgt | 33 +++- src/bosses/unicorn/unicorn_boss.nvgt | 25 ++- src/constants.nvgt | 3 + src/learn_sounds.nvgt | 3 +- src/menus/character_info.nvgt | 8 +- src/pet_system.nvgt | 286 ++++++++++++++++++++++++--- src/save_system.nvgt | 18 ++ 9 files changed, 349 insertions(+), 37 deletions(-) diff --git a/draugnorak.nvgt b/draugnorak.nvgt index 6f8b08c..3abe746 100755 --- a/draugnorak.nvgt +++ b/draugnorak.nvgt @@ -318,7 +318,7 @@ void run_game() // Health Key if (key_pressed(KEY_H)) { - speak_with_history(player_health + " health of " + max_health, true); + speak_with_history(get_health_report(), true); } // Coordinates Key diff --git a/files/credits.md b/files/credits.md index 5ff7e3a..528b5c6 100644 --- a/files/credits.md +++ b/files/credits.md @@ -1,10 +1,12 @@ Billy Wolfe: Designer, coder, and sounds https://stormux.org -Zack Benton: Sounds - -Michael Taboada: sounds +Chris Wright: Beta Tester Deedra Waters: Beta Tester +Michael Taboada: sounds + Sarah Russell: Beta Tester + +Zack Benton: Beta Tester, Mac Support, Sounds diff --git a/src/bosses/bandit_hideout.nvgt b/src/bosses/bandit_hideout.nvgt index d359422..fe0dee1 100644 --- a/src/bosses/bandit_hideout.nvgt +++ b/src/bosses/bandit_hideout.nvgt @@ -143,6 +143,32 @@ HideoutBandit@ get_hideout_bandit_at(int pos) { return null; } +bool pet_find_hideout_target(int originPos, int referencePos, int &out targetPos, string &out targetLabel, int &out targetKind) { + int bestDistance = PET_RANGE + 1; + targetPos = -1; + targetLabel = ""; + targetKind = -1; + + for (uint i = 0; i < hideoutBandits.length(); i++) { + int distanceToOrigin = abs(hideoutBandits[i].position - originPos); + if (distanceToOrigin > PET_RANGE) continue; + int distance = abs(hideoutBandits[i].position - referencePos); + if (distance < bestDistance) { + bestDistance = distance; + targetPos = hideoutBandits[i].position; + targetLabel = "bandit"; + targetKind = 0; + } + } + + return targetPos != -1; +} + +bool pet_damage_hideout_target(int targetKind, int targetPos, int damage) { + if (targetKind != 0) return false; + return damage_hideout_bandit_at(targetPos, damage); +} + int clamp_hideout_spawn_start(int startX) { if (startX < 0) return 0; if (startX >= BANDIT_HIDEOUT_MAP_SIZE) return BANDIT_HIDEOUT_MAP_SIZE - 1; @@ -236,6 +262,7 @@ void init_bandit_hideout_adventure() { void cleanup_bandit_hideout_adventure() { clear_hideout_bandits(); + end_pet_adventure(); reset_adventure_combat_state(); p.destroy_all(); } @@ -258,6 +285,8 @@ void run_bandit_hideout_adventure() { intro.insert_last("Bandits will, of course, not take this lying down."); text_reader_lines(intro, "Adventure", true); + begin_pet_adventure(@pet_find_hideout_target, @pet_damage_hideout_target, hideoutPlayerX); + bool adventurePaused = false; while (true) { wait(5); @@ -296,7 +325,7 @@ void run_bandit_hideout_adventure() { check_speech_history_keys(); if (key_pressed(KEY_H)) { - speak_with_history(player_health + " health of " + max_health, true); + speak_with_history(get_health_report(), true); } if (key_pressed(KEY_X)) { @@ -312,6 +341,8 @@ void run_bandit_hideout_adventure() { update_hideout_search(); update_hideout_bandits(); + update_pet_adventure_position(hideoutPlayerX); + update_pets(); adventure_update_bow_shot(hideoutPlayerX); if (hideoutBarricadeHealth <= 0) { diff --git a/src/bosses/unicorn/unicorn_boss.nvgt b/src/bosses/unicorn/unicorn_boss.nvgt index ac6ce81..be434d2 100644 --- a/src/bosses/unicorn/unicorn_boss.nvgt +++ b/src/bosses/unicorn/unicorn_boss.nvgt @@ -86,6 +86,7 @@ void init_unicorn_adventure() { void cleanup_unicorn_adventure() { p.destroy_all(); reset_adventure_combat_state(); + end_pet_adventure(); if (unicorn.sound_handle != -1) { p.destroy_sound(unicorn.sound_handle); unicorn.sound_handle = -1; @@ -118,6 +119,8 @@ void run_unicorn_adventure() { intro.insert_last("Controls: LEFT/RIGHT to move, UP to jump, CTRL to attack, ESC to flee"); text_reader_lines(intro, "Adventure", true); + begin_pet_adventure(@pet_find_unicorn_target, @pet_damage_unicorn_target, player_arena_x); + // Adventure Loop bool adventurePaused = false; while (true) { @@ -159,7 +162,7 @@ void run_unicorn_adventure() { // Health if (key_pressed(KEY_H)) { - speak_with_history(player_health + " health of " + max_health, true); + speak_with_history(get_health_report(), true); } // Coordinates @@ -177,6 +180,8 @@ void run_unicorn_adventure() { update_unicorn(); adventure_update_bow_shot(player_arena_x); update_unicorn_weapon_range_audio(); + update_pet_adventure_position(player_arena_x); + update_pets(); // Check Conditions - unicorn falls when on collapsed bridge if (!unicorn_defeated && bridge_collapsed && unicorn.x >= BRIDGE_START && unicorn.x <= BRIDGE_END) { @@ -496,6 +501,24 @@ void apply_unicorn_damage(int damage) { } } +bool pet_find_unicorn_target(int originPos, int referencePos, int &out targetPos, string &out targetLabel, int &out targetKind) { + targetPos = -1; + targetLabel = ""; + targetKind = -1; + if (unicorn.health <= 0) return false; + if (abs(unicorn.x - originPos) > PET_RANGE) return false; + targetPos = unicorn.x; + targetLabel = "unicorn"; + targetKind = 0; + return true; +} + +bool pet_damage_unicorn_target(int targetKind, int targetPos, int damage) { + if (targetKind != 0) return false; + apply_unicorn_damage(damage); + return true; +} + void play_unicorn_ground_death_sequence() { if (unicorn.sound_handle != -1) { p.destroy_sound(unicorn.sound_handle); diff --git a/src/constants.nvgt b/src/constants.nvgt index 4563c49..f084f3b 100644 --- a/src/constants.nvgt +++ b/src/constants.nvgt @@ -266,6 +266,9 @@ const int PET_LOYALTY_BONUS_THRESHOLD = 5; const int PET_MOVE_SPEED = 320; // Slightly faster than base walk speed const int PET_TRAVEL_MIN_MS = 100; const int PET_RANGE = BOW_RANGE + 2; +const int PET_HEALTH_MAX = 10; +const int PET_ATTACK_SELF_DAMAGE = 1; +const int PET_KNOCKOUT_COOLDOWN_HOURS = 3; // Goose settings const int GOOSE_HEALTH = 1; diff --git a/src/learn_sounds.nvgt b/src/learn_sounds.nvgt index 9de7829..681560a 100644 --- a/src/learn_sounds.nvgt +++ b/src/learn_sounds.nvgt @@ -13,7 +13,8 @@ string[] learnSoundSkipList = { "sounds/actions/fishpole.ogg", "sounds/actions/hit_ground.ogg", "sounds/menu/", - "sounds/nature/" + "sounds/nature/", + "sounds/pets/" }; // Description entries: keep paths/texts aligned by index. diff --git a/src/menus/character_info.nvgt b/src/menus/character_info.nvgt index 6bef64c..cffff12 100644 --- a/src/menus/character_info.nvgt +++ b/src/menus/character_info.nvgt @@ -39,7 +39,13 @@ void run_character_info_menu() { options.insert_last("Favor " + format_favor(favor) + "."); options.insert_last("Speed " + get_speed_status() + "."); if (petActive) { - options.insert_last("Pet " + petType + ". Gender " + petGender + ". Loyalty " + petLoyalty + "."); + string petInfo = "Pet " + get_pet_display_name() + ". " + petHealth + " health of " + PET_HEALTH_MAX + "."; + petInfo += " Loyalty " + petLoyalty + " of " + PET_LOYALTY_MAX + "."; + if (petKnockoutHoursRemaining > 0) { + string hourLabel = (petKnockoutHoursRemaining == 1) ? "hour" : "hours"; + petInfo += " Knocked out. " + petKnockoutHoursRemaining + " " + hourLabel + " remaining."; + } + options.insert_last(petInfo); } else { options.insert_last("Pet none."); } diff --git a/src/pet_system.nvgt b/src/pet_system.nvgt index 70354f0..6663697 100644 --- a/src/pet_system.nvgt +++ b/src/pet_system.nvgt @@ -9,6 +9,8 @@ int petLoyalty = 0; bool petOut = false; int petPosition = 0; bool petPositionValid = false; +int petHealth = 0; +int petKnockoutHoursRemaining = 0; timer petAttackTimer; timer petRetrieveTimer; @@ -35,6 +37,17 @@ timer petTravelTimer; string[] petSoundPaths; bool petSoundsInitialized = false; +funcdef bool PetAdventureFindTargetCallback(int originPos, int referencePos, int &out targetPos, string &out targetLabel, int &out targetKind); +funcdef bool PetAdventureDamageCallback(int targetKind, int targetPos, int damage); + +bool petAdventureMode = false; +int petAdventurePlayerPos = -1; +bool petAdventurePreOut = false; +int petAdventurePrePosition = 0; +bool petAdventurePrePositionValid = false; +PetAdventureFindTargetCallback@ petAdventureFindTarget = null; +PetAdventureDamageCallback@ petAdventureDamageTarget = null; + string normalize_pet_path(const string&in path) { return path.replace("\\", "/", true); } @@ -47,6 +60,15 @@ string collapse_pet_spaces(const string&in text) { return result; } +string normalize_pet_name_text(const string&in raw) { + string name = raw; + name = name.replace("_", " ", true); + name = name.replace("-", " ", true); + name = collapse_pet_spaces(name); + name.trim_whitespace_this(); + return name; +} + void gather_pet_sound_files(const string&in basePath, string[]@ outFiles) { string[]@ files = find_files(basePath + "/*.ogg"); if (@files !is null) { @@ -179,6 +201,89 @@ void queue_pet_event(const string&in message, int soundPos = -1) { petEventPositions.insert_last(soundPos); } +string get_pet_display_name() { + string name = normalize_pet_name_text(petType); + if (name == "") return "pet"; + return name; +} + +int get_pet_listener_pos() { + if (petAdventurePlayerPos >= 0) return petAdventurePlayerPos; + return x; +} + +string get_health_report() { + string report = player_health + " health of " + max_health; + if (petActive) { + report += ", " + get_pet_display_name() + ", " + petHealth + " health of " + PET_HEALTH_MAX; + report += ", loyalty " + petLoyalty + " of " + PET_LOYALTY_MAX; + } + return report; +} + +int get_pet_search_origin() { + if (petOut && petPositionValid) return petPosition; + return get_pet_listener_pos(); +} + +bool is_pet_knocked_out() { + return petKnockoutHoursRemaining > 0 || petHealth <= 0; +} + +void begin_pet_adventure(PetAdventureFindTargetCallback@ findTarget, PetAdventureDamageCallback@ damageTarget, int playerPos) { + petAdventureMode = true; + petAdventurePlayerPos = playerPos; + petAdventurePreOut = petOut; + petAdventurePrePosition = petPosition; + petAdventurePrePositionValid = petPositionValid; + @petAdventureFindTarget = findTarget; + @petAdventureDamageTarget = damageTarget; + stop_pet_travel(); + petOut = false; + petPositionValid = false; +} + +void update_pet_adventure_position(int playerPos) { + petAdventurePlayerPos = playerPos; +} + +void end_pet_adventure() { + petAdventureMode = false; + petAdventurePlayerPos = -1; + @petAdventureFindTarget = null; + @petAdventureDamageTarget = null; + stop_pet_travel(); + if (is_pet_knocked_out()) { + petOut = false; + petPositionValid = false; + } else if (petAdventurePreOut) { + petOut = true; + petPosition = petAdventurePrePosition; + petPositionValid = petAdventurePrePositionValid; + } else { + petOut = false; + petPositionValid = false; + } + petAdventurePreOut = false; + petAdventurePrePosition = 0; + petAdventurePrePositionValid = false; +} + +void queue_pet_return_event() { + if (!petActive) return; + queue_pet_event("A " + get_pet_display_name() + " returns to you."); +} + +void knock_out_pet() { + if (!petActive) return; + petHealth = 0; + petKnockoutHoursRemaining = PET_KNOCKOUT_COOLDOWN_HOURS; + petOut = false; + petPositionValid = false; + stop_pet_travel(); + queue_pet_event("Your " + get_pet_display_name() + " has been knocked out."); +} + void update_pet_events() { if (petEventMessages.length() == 0) { petEventSoundHandle = -1; @@ -202,7 +307,7 @@ void update_pet_events() { int soundPos = petEventPositions[0]; if (soundPath != "" && file_exists(soundPath)) { if (soundPos >= 0) { - petEventSoundHandle = play_1d_with_volume_step(soundPath, x, soundPos, false, PLAYER_WEAPON_SOUND_VOLUME_STEP); + petEventSoundHandle = play_1d_with_volume_step(soundPath, get_pet_listener_pos(), soundPos, false, PLAYER_WEAPON_SOUND_VOLUME_STEP); } else { petEventSoundHandle = p.play_stationary(soundPath, false); } @@ -227,9 +332,14 @@ void reset_pet_state() { petOut = false; petPosition = 0; petPositionValid = false; + petHealth = PET_HEALTH_MAX; + petKnockoutHoursRemaining = 0; + petHealth = 0; + petKnockoutHoursRemaining = 0; petEventMessages.resize(0); petEventSounds.resize(0); petEventPositions.resize(0); + safe_destroy_sound(petEventSoundHandle); petEventSoundHandle = -1; petTravelActive = false; petTravelAction = PET_TRAVEL_NONE; @@ -246,7 +356,7 @@ void reset_pet_state() { } void pet_leave() { - string oldPet = petType; + string oldPet = normalize_pet_name_text(petType); reset_pet_state(); if (oldPet != "") { speak_with_history(oldPet + " leaves.", true); @@ -273,17 +383,26 @@ void check_pet_call_key() { speak_with_history("No pet.", true); return; } + if (is_pet_knocked_out()) { + string message = "Your " + get_pet_display_name() + " has been knocked out."; + if (petKnockoutHoursRemaining > 0) { + string hourLabel = (petKnockoutHoursRemaining == 1) ? "hour" : "hours"; + message += " " + petKnockoutHoursRemaining + " " + hourLabel + " remaining."; + } + speak_with_history(message, true); + return; + } if (petOut) { petOut = false; stop_pet_travel(); petPositionValid = false; - speak_with_history("Pet called back.", true); + queue_pet_return_event(); return; } adjust_pet_loyalty(-PET_LOYALTY_CALLOUT_COST); if (!petActive) return; petOut = true; - petPosition = x; + petPosition = get_pet_listener_pos(); petPositionValid = true; if (file_exists("sounds/pets/call_pet.ogg")) { p.play_stationary("sounds/pets/call_pet.ogg", false); @@ -299,10 +418,12 @@ void adopt_pet(const string&in soundPath) { petOut = false; petPosition = 0; petPositionValid = false; + petHealth = PET_HEALTH_MAX; + petKnockoutHoursRemaining = 0; petAttackTimer.restart(); petRetrieveTimer.restart(); petTravelTimer.restart(); - speak_with_history("A " + petType + " joins you.", true); + speak_with_history("A " + get_pet_display_name() + " joins you.", true); } void stop_pet_travel() { @@ -328,7 +449,7 @@ void start_pet_travel_attack(int targetPos, const string&in targetLabel, int tar stop_pet_travel(); petTravelActive = true; petTravelAction = PET_TRAVEL_ATTACK; - petTravelStartPos = petPositionValid ? petPosition : x; + petTravelStartPos = petPositionValid ? petPosition : get_pet_listener_pos(); petTravelTargetPos = targetPos; petTravelTargetLabel = targetLabel; petTravelTargetKind = targetKind; @@ -338,7 +459,7 @@ void start_pet_travel_attack(int targetPos, const string&in targetLabel, int tar if (petSoundPath != "" && file_exists(petSoundPath)) { petTravelSoundHandle = play_1d_with_volume_step( petSoundPath, - x, + get_pet_listener_pos(), petTravelStartPos, true, PLAYER_WEAPON_SOUND_VOLUME_STEP @@ -350,7 +471,7 @@ void start_pet_travel_retrieve(int targetPos) { stop_pet_travel(); petTravelActive = true; petTravelAction = PET_TRAVEL_RETRIEVE; - petTravelStartPos = petPositionValid ? petPosition : x; + petTravelStartPos = petPositionValid ? petPosition : get_pet_listener_pos(); petTravelTargetPos = targetPos; petTravelDurationMs = get_pet_travel_duration_ms(petTravelStartPos, targetPos); petTravelTimer.restart(); @@ -360,6 +481,13 @@ void update_pet_travel() { if (!petTravelActive) return; if (petTravelDurationMs < 1) petTravelDurationMs = 1; + if (petTravelAction == PET_TRAVEL_ATTACK) { + int refreshedTargetPos = -1; + if (find_pet_attack_target_by_kind(petTravelTargetKind, petTravelTargetPos, refreshedTargetPos)) { + petTravelTargetPos = refreshedTargetPos; + } + } + int elapsed = petTravelTimer.elapsed; float progress = float(elapsed) / float(petTravelDurationMs); if (progress > 1.0f) progress = 1.0f; @@ -378,24 +506,40 @@ void update_pet_travel() { if (petTravelAction == PET_TRAVEL_ATTACK) { int damage = BOW_DAMAGE_MAX; bool hit = false; - if (petTravelTargetKind == 0) { - hit = damage_bandit_at(petTravelTargetPos, damage); - } else if (petTravelTargetKind == 1) { - hit = damage_undead_at(petTravelTargetPos, damage); - } else if (petTravelTargetKind == 2) { - hit = damage_boar_at(petTravelTargetPos, damage); + if (petAdventureMode && @petAdventureDamageTarget !is null) { + hit = petAdventureDamageTarget(petTravelTargetKind, petTravelTargetPos, damage); + } else { + if (petTravelTargetKind == 0) { + hit = damage_bandit_at(petTravelTargetPos, damage); + } else if (petTravelTargetKind == 1) { + hit = damage_undead_at(petTravelTargetPos, damage); + } else if (petTravelTargetKind == 2) { + hit = damage_boar_at(petTravelTargetPos, damage); + } } if (hit) { - queue_pet_event("Your " + petType + " attacks the " + petTravelTargetLabel + ".", petTravelTargetPos); + petHealth -= PET_ATTACK_SELF_DAMAGE; + if (petHealth <= 0) { + knock_out_pet(); + return; + } } petPosition = petTravelTargetPos; petPositionValid = true; int nextTargetPos = -1; string nextTargetLabel = ""; int nextTargetKind = -1; - if (!find_pet_attack_target(nextTargetPos, nextTargetLabel, nextTargetKind)) { + bool hasNextTarget = find_pet_attack_target(nextTargetPos, nextTargetLabel, nextTargetKind); + if (!hasNextTarget) { + WorldDrop@ drop = find_pet_drop_target(); + if (drop !is null) { + petRetrieveTimer.restart(); + start_pet_travel_retrieve(drop.position); + return; + } petOut = false; petPositionValid = false; + queue_pet_return_event(); } } else if (petTravelAction == PET_TRAVEL_RETRIEVE) { WorldDrop@ drop = get_drop_at(petTravelTargetPos); @@ -404,8 +548,25 @@ void update_pet_travel() { if (try_pet_pickup_world_drop(drop, message)) { remove_drop_at(drop.position); queue_pet_event(message); + petPosition = petTravelTargetPos; + petPositionValid = true; + WorldDrop@ nextDrop = find_pet_drop_target(); + if (nextDrop !is null) { + petRetrieveTimer.restart(); + start_pet_travel_retrieve(nextDrop.position); + return; + } + int nextTargetPos = -1; + string nextTargetLabel = ""; + int nextTargetKind = -1; + if (find_pet_attack_target(nextTargetPos, nextTargetLabel, nextTargetKind)) { + petAttackTimer.restart(); + start_pet_travel_attack(nextTargetPos, nextTargetLabel, nextTargetKind); + return; + } petOut = false; petPositionValid = false; + queue_pet_return_event(); } } } @@ -495,11 +656,12 @@ void attempt_pet_offer_from_tree() { bool try_pet_pickup_small_game(const string&in gameType, string &out message) { if (get_personal_count(ITEM_SMALL_GAME) >= get_personal_stack_limit()) { - return false; + message = "You can't carry a " + gameType + ", so you give it to a " + get_pet_display_name() + " to chew on."; + return true; } add_personal_count(ITEM_SMALL_GAME, 1); personal_small_game_types.insert_last(gameType); - message = "Your " + petType + " retrieved " + gameType + "."; + message = "Your " + get_pet_display_name() + " retrieved " + gameType + ". A " + get_pet_display_name() + " returns to you."; return true; } @@ -511,21 +673,24 @@ bool try_pet_pickup_world_drop(WorldDrop@ drop, string &out message) { if (drop.type == "arrow") { int maxArrows = get_arrow_limit(); if (maxArrows <= 0) { - return false; + message = "You can't carry an arrow, so you give it to a " + get_pet_display_name() + " to chew on."; + return true; } if (get_personal_count(ITEM_ARROWS) >= maxArrows) { - return false; + message = "You can't carry an arrow, so you give it to a " + get_pet_display_name() + " to chew on."; + return true; } add_personal_count(ITEM_ARROWS, 1); - message = "Your " + petType + " retrieved an arrow."; + message = "Your " + get_pet_display_name() + " retrieved an arrow. A " + get_pet_display_name() + " returns to you."; return true; } if (drop.type == "boar carcass") { if (get_personal_count(ITEM_BOAR_CARCASSES) >= get_personal_stack_limit()) { - return false; + message = "You can't carry a boar carcass, so you give it to a " + get_pet_display_name() + " to chew on."; + return true; } add_personal_count(ITEM_BOAR_CARCASSES, 1); - message = "Your " + petType + " retrieved a boar carcass."; + message = "Your " + get_pet_display_name() + " retrieved a boar carcass. A " + get_pet_display_name() + " returns to you."; return true; } return false; @@ -534,8 +699,9 @@ bool try_pet_pickup_world_drop(WorldDrop@ drop, string &out message) { WorldDrop@ find_pet_drop_target() { int bestDistance = PET_RANGE + 1; WorldDrop@ best = null; + int origin = get_pet_search_origin(); for (uint i = 0; i < world_drops.length(); i++) { - int distance = abs(world_drops[i].position - x); + int distance = abs(world_drops[i].position - origin); if (distance > PET_RANGE) continue; if (distance < bestDistance) { bestDistance = distance; @@ -549,6 +715,8 @@ void update_pet_retrieval() { if (!petActive) return; if (!petOut) return; if (petLoyalty <= 0) return; + if (petAdventureMode) return; + if (is_pet_knocked_out()) return; if (petRetrieveTimer.elapsed < PET_RETRIEVE_COOLDOWN) return; if (petEventMessages.length() > 2) return; if (petTravelActive) return; @@ -565,9 +733,13 @@ bool find_pet_attack_target(int &out targetPos, string &out targetLabel, int &ou targetPos = -1; targetLabel = ""; targetKind = -1; + int origin = get_pet_search_origin(); + if (petAdventureMode && @petAdventureFindTarget !is null) { + return petAdventureFindTarget(origin, origin, targetPos, targetLabel, targetKind); + } for (uint i = 0; i < bandits.length(); i++) { - int distance = abs(bandits[i].position - x); + int distance = abs(bandits[i].position - origin); if (distance > PET_RANGE) continue; if (distance < bestDistance) { bestDistance = distance; @@ -579,7 +751,7 @@ bool find_pet_attack_target(int &out targetPos, string &out targetLabel, int &ou for (uint i = 0; i < undeads.length(); i++) { if (undeads[i].undead_type == "undead_resident") continue; - int distance = abs(undeads[i].position - x); + int distance = abs(undeads[i].position - origin); if (distance > PET_RANGE) continue; if (distance < bestDistance) { bestDistance = distance; @@ -590,7 +762,7 @@ bool find_pet_attack_target(int &out targetPos, string &out targetLabel, int &ou } for (uint i = 0; i < ground_games.length(); i++) { - int distance = abs(ground_games[i].position - x); + int distance = abs(ground_games[i].position - origin); if (distance > PET_RANGE) continue; if (distance < bestDistance) { bestDistance = distance; @@ -603,10 +775,57 @@ bool find_pet_attack_target(int &out targetPos, string &out targetLabel, int &ou return targetPos != -1; } +bool find_pet_attack_target_by_kind(int targetKind, int referencePos, int &out targetPos) { + int bestDistance = PET_RANGE + 1; + targetPos = -1; + int origin = get_pet_search_origin(); + if (petAdventureMode && @petAdventureFindTarget !is null) { + string targetLabel = ""; + int foundKind = -1; + return petAdventureFindTarget(origin, referencePos, targetPos, targetLabel, foundKind); + } + + if (targetKind == 0) { + for (uint i = 0; i < bandits.length(); i++) { + int distanceToOrigin = abs(bandits[i].position - origin); + if (distanceToOrigin > PET_RANGE) continue; + int distance = abs(bandits[i].position - referencePos); + if (distance < bestDistance) { + bestDistance = distance; + targetPos = bandits[i].position; + } + } + } else if (targetKind == 1) { + for (uint i = 0; i < undeads.length(); i++) { + if (undeads[i].undead_type == "undead_resident") continue; + int distanceToOrigin = abs(undeads[i].position - origin); + if (distanceToOrigin > PET_RANGE) continue; + int distance = abs(undeads[i].position - referencePos); + if (distance < bestDistance) { + bestDistance = distance; + targetPos = undeads[i].position; + } + } + } else if (targetKind == 2) { + for (uint i = 0; i < ground_games.length(); i++) { + int distanceToOrigin = abs(ground_games[i].position - origin); + if (distanceToOrigin > PET_RANGE) continue; + int distance = abs(ground_games[i].position - referencePos); + if (distance < bestDistance) { + bestDistance = distance; + targetPos = ground_games[i].position; + } + } + } + + return targetPos != -1; +} + void update_pet_attack() { if (!petActive) return; if (!petOut) return; if (petLoyalty <= 0) return; + if (is_pet_knocked_out()) return; if (petAttackTimer.elapsed < PET_ATTACK_COOLDOWN) return; if (petTravelActive) return; @@ -632,7 +851,7 @@ 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 " + petType + " retrieved " + added + " " + itemName + ".", x); + queue_pet_event("Your " + get_pet_display_name() + " retrieved " + added + " " + itemName + ". A " + get_pet_display_name() + " returns to you.", x); petOut = false; petPositionValid = false; } @@ -640,7 +859,16 @@ void attempt_pet_random_find() { void handle_pet_hourly_update(int hour) { if (!petActive) return; - if (get_pet_food_personal_total() == 0) { + if (petKnockoutHoursRemaining > 0) { + petKnockoutHoursRemaining--; + if (petKnockoutHoursRemaining <= 0) { + petKnockoutHoursRemaining = 0; + petHealth = PET_HEALTH_MAX; + notify("Your " + get_pet_display_name() + " has recovered from its injuries."); + } + } + + if (!has_pet_food_available()) { adjust_pet_loyalty(-PET_LOYALTY_HUNGER_LOSS); } diff --git a/src/save_system.nvgt b/src/save_system.nvgt index 4c03765..75af86f 100644 --- a/src/save_system.nvgt +++ b/src/save_system.nvgt @@ -806,6 +806,8 @@ bool save_game_state() { saveData.set("pet_type", petType); saveData.set("pet_gender", petGender); saveData.set("pet_loyalty", petLoyalty); + saveData.set("pet_health", petHealth); + saveData.set("pet_ko_hours_remaining", petKnockoutHoursRemaining); // Save inventory arrays using new compact format saveData.set("personal_inventory", serialize_inventory_array(personal_inventory)); @@ -1129,11 +1131,15 @@ bool load_game_state_from_file(const string&in filename) { petGender = loadedPetGender; } petLoyalty = int(get_number(saveData, "pet_loyalty", 0)); + petHealth = int(get_number(saveData, "pet_health", PET_HEALTH_MAX)); + petKnockoutHoursRemaining = int(get_number(saveData, "pet_ko_hours_remaining", 0)); if (!petActive) { petSoundPath = ""; petType = ""; petGender = ""; petLoyalty = 0; + petHealth = 0; + petKnockoutHoursRemaining = 0; } if (petActive && petSoundPath != "" && !file_exists(petSoundPath)) { petActive = false; @@ -1141,8 +1147,20 @@ bool load_game_state_from_file(const string&in filename) { petType = ""; petGender = ""; petLoyalty = 0; + petHealth = 0; + petKnockoutHoursRemaining = 0; } if (petActive) { + if (petKnockoutHoursRemaining < 0) petKnockoutHoursRemaining = 0; + if (petKnockoutHoursRemaining > PET_KNOCKOUT_COOLDOWN_HOURS) { + petKnockoutHoursRemaining = PET_KNOCKOUT_COOLDOWN_HOURS; + } + if (petKnockoutHoursRemaining > 0) { + petHealth = 0; + } else { + if (petHealth <= 0) petHealth = PET_HEALTH_MAX; + if (petHealth > PET_HEALTH_MAX) petHealth = PET_HEALTH_MAX; + } petAttackTimer.restart(); petRetrieveTimer.restart(); }