// Pet system // Handles pet acquisition, loyalty, feeding, retrieval, and combat support bool petActive = false; string petSoundPath = ""; string petType = ""; string petGender = ""; int petLoyalty = 0; bool petOut = false; timer petAttackTimer; timer petRetrieveTimer; string[] petEventMessages; string[] petEventSounds; int[] petEventPositions; int petEventSoundHandle = -1; const int PET_TRAVEL_NONE = 0; const int PET_TRAVEL_ATTACK = 1; const int PET_TRAVEL_RETRIEVE = 2; bool petTravelActive = false; int petTravelAction = PET_TRAVEL_NONE; int petTravelStartPos = 0; int petTravelTargetPos = -1; int petTravelTargetKind = -1; string petTravelTargetLabel = ""; int petTravelDurationMs = 0; int petTravelSoundHandle = -1; timer petTravelTimer; string[] petSoundPaths; bool petSoundsInitialized = false; string normalize_pet_path(const string&in path) { return path.replace("\\", "/", true); } string collapse_pet_spaces(const string&in text) { string result = text; while (result.find_first(" ") > -1) { result = result.replace(" ", " ", true); } return result; } void gather_pet_sound_files(const string&in basePath, string[]@ outFiles) { string[]@ files = find_files(basePath + "/*.ogg"); if (@files !is null) { for (uint i = 0; i < files.length(); i++) { outFiles.insert_last(basePath + "/" + files[i]); } } string[]@ folders = find_directories(basePath + "/*"); if (@folders !is null) { for (uint i = 0; i < folders.length(); i++) { gather_pet_sound_files(basePath + "/" + folders[i], outFiles); } } } bool sort_pet_sound_paths(const string &in a, const string &in b) { return a.lower() < b.lower(); } void init_pet_sounds() { if (petSoundsInitialized) return; petSoundsInitialized = true; petSoundPaths.resize(0); if (!directory_exists("sounds/pets")) return; gather_pet_sound_files("sounds/pets", petSoundPaths); if (petSoundPaths.length() > 1) { petSoundPaths.sort(sort_pet_sound_paths); } } string get_pet_name_from_sound_path(const string&in soundPath) { string normalizedPath = normalize_pet_path(soundPath); int slashPos = normalizedPath.find_last_of("/"); string name = (slashPos >= 0) ? normalizedPath.substr(slashPos + 1) : normalizedPath; string lowerName = name.lower(); if (lowerName.length() > 4 && lowerName.substr(lowerName.length() - 4) == ".ogg") { name = name.substr(0, name.length() - 4); } name = name.replace("_", " ", true); name = name.replace("-", " ", true); name = collapse_pet_spaces(name); name = name.lower(); name.trim_whitespace_this(); if (name.length() == 0) return "Pet"; string first = name.substr(0, 1).upper(); if (name.length() == 1) return first; return first + name.substr(1); } string get_pet_sound_for_name(const string&in petName) { init_pet_sounds(); string target = petName.lower(); for (uint i = 0; i < petSoundPaths.length(); i++) { string normalizedPath = normalize_pet_path(petSoundPaths[i]); int slashPos = normalizedPath.find_last_of("/"); string fileName = (slashPos >= 0) ? normalizedPath.substr(slashPos + 1) : normalizedPath; string lowerName = fileName.lower(); if (lowerName.length() > 4 && lowerName.substr(lowerName.length() - 4) == ".ogg") { lowerName = lowerName.substr(0, lowerName.length() - 4); } if (lowerName == target) { return petSoundPaths[i]; } } return ""; } int get_pet_food_personal_total() { return get_personal_count(ITEM_MEAT) + get_personal_count(ITEM_SMOKED_FISH) + get_personal_count(ITEM_FISH) + get_personal_count(ITEM_BASKET_FOOD); } int get_pet_food_storage_total() { return get_storage_count(ITEM_MEAT) + get_storage_count(ITEM_SMOKED_FISH) + get_storage_count(ITEM_FISH) + get_storage_count(ITEM_BASKET_FOOD); } bool has_pet_food_available() { return get_pet_food_personal_total() + get_pet_food_storage_total() > 0; } bool consume_pet_food() { if (get_personal_count(ITEM_MEAT) > 0) { add_personal_count(ITEM_MEAT, -1); return true; } if (get_personal_count(ITEM_SMOKED_FISH) > 0) { add_personal_count(ITEM_SMOKED_FISH, -1); return true; } if (get_personal_count(ITEM_FISH) > 0) { pop_personal_fish_weight(); add_personal_count(ITEM_FISH, -1); return true; } if (get_personal_count(ITEM_BASKET_FOOD) > 0) { add_personal_count(ITEM_BASKET_FOOD, -1); return true; } if (get_storage_count(ITEM_MEAT) > 0) { add_storage_count(ITEM_MEAT, -1); return true; } if (get_storage_count(ITEM_SMOKED_FISH) > 0) { add_storage_count(ITEM_SMOKED_FISH, -1); return true; } if (get_storage_count(ITEM_FISH) > 0) { pop_storage_fish_weight(); add_storage_count(ITEM_FISH, -1); return true; } if (get_storage_count(ITEM_BASKET_FOOD) > 0) { add_storage_count(ITEM_BASKET_FOOD, -1); return true; } return false; } void queue_pet_event(const string&in message, int soundPos = -1) { if (!petActive) return; if (message.length() == 0) return; petEventMessages.insert_last(message); petEventSounds.insert_last(petSoundPath); petEventPositions.insert_last(soundPos); } void update_pet_events() { if (petEventMessages.length() == 0) { petEventSoundHandle = -1; return; } if (petEventSoundHandle != -1) { if (p.sound_is_active(petEventSoundHandle)) { return; } string message = petEventMessages[0]; petEventMessages.remove_at(0); petEventSounds.remove_at(0); petEventPositions.remove_at(0); petEventSoundHandle = -1; speak_with_history(message, true); return; } string soundPath = petEventSounds[0]; 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); } else { petEventSoundHandle = p.play_stationary(soundPath, false); } if (petEventSoundHandle != -1) { return; } } string message = petEventMessages[0]; petEventMessages.remove_at(0); petEventSounds.remove_at(0); petEventPositions.remove_at(0); speak_with_history(message, true); } void reset_pet_state() { petActive = false; petSoundPath = ""; petType = ""; petGender = ""; petLoyalty = 0; petOut = false; petEventMessages.resize(0); petEventSounds.resize(0); petEventPositions.resize(0); petEventSoundHandle = -1; petTravelActive = false; petTravelAction = PET_TRAVEL_NONE; petTravelStartPos = 0; petTravelTargetPos = -1; petTravelTargetKind = -1; petTravelTargetLabel = ""; petTravelDurationMs = 0; safe_destroy_sound(petTravelSoundHandle); petTravelSoundHandle = -1; petAttackTimer.restart(); petRetrieveTimer.restart(); petTravelTimer.restart(); } void pet_leave() { string oldPet = petType; reset_pet_state(); if (oldPet != "") { speak_with_history(oldPet + " leaves.", true); } } void clamp_pet_loyalty() { if (petLoyalty < 0) petLoyalty = 0; if (petLoyalty > PET_LOYALTY_MAX) petLoyalty = PET_LOYALTY_MAX; } void adjust_pet_loyalty(int delta) { if (!petActive) return; petLoyalty += delta; clamp_pet_loyalty(); if (petLoyalty <= 0) { pet_leave(); } } void check_pet_call_key() { if (!key_pressed(KEY_SPACE)) return; if (!petActive) { speak_with_history("No pet.", true); return; } if (petOut) { petOut = false; stop_pet_travel(); speak_with_history("Pet called back.", true); return; } petOut = true; if (file_exists("sounds/pets/call_pet.ogg")) { p.play_stationary("sounds/pets/call_pet.ogg", false); } } void adopt_pet(const string&in soundPath) { petActive = true; petSoundPath = soundPath; petType = get_pet_name_from_sound_path(soundPath); petGender = (random(0, 1) == 0) ? "Male" : "Female"; petLoyalty = PET_START_LOYALTY; petOut = false; petAttackTimer.restart(); petRetrieveTimer.restart(); petTravelTimer.restart(); speak_with_history("A " + petType + " joins you.", true); } void stop_pet_travel() { petTravelActive = false; petTravelAction = PET_TRAVEL_NONE; petTravelStartPos = 0; petTravelTargetPos = -1; petTravelTargetKind = -1; petTravelTargetLabel = ""; petTravelDurationMs = 0; safe_destroy_sound(petTravelSoundHandle); petTravelSoundHandle = -1; } int get_pet_travel_duration_ms(int targetPos) { int distance = abs(targetPos - x); int duration = distance * PET_MOVE_SPEED; if (duration < PET_TRAVEL_MIN_MS) duration = PET_TRAVEL_MIN_MS; return duration; } void start_pet_travel_attack(int targetPos, const string&in targetLabel, int targetKind) { stop_pet_travel(); petTravelActive = true; petTravelAction = PET_TRAVEL_ATTACK; petTravelStartPos = x; petTravelTargetPos = targetPos; petTravelTargetLabel = targetLabel; petTravelTargetKind = targetKind; petTravelDurationMs = get_pet_travel_duration_ms(targetPos); petTravelTimer.restart(); if (petSoundPath != "" && file_exists(petSoundPath)) { petTravelSoundHandle = play_1d_with_volume_step( petSoundPath, x, petTravelStartPos, true, PLAYER_WEAPON_SOUND_VOLUME_STEP ); } } void start_pet_travel_retrieve(int targetPos) { stop_pet_travel(); petTravelActive = true; petTravelAction = PET_TRAVEL_RETRIEVE; petTravelStartPos = x; petTravelTargetPos = targetPos; petTravelDurationMs = get_pet_travel_duration_ms(targetPos); petTravelTimer.restart(); } void update_pet_travel() { if (!petTravelActive) return; if (petTravelDurationMs < 1) petTravelDurationMs = 1; int elapsed = petTravelTimer.elapsed; float progress = float(elapsed) / float(petTravelDurationMs); if (progress > 1.0f) progress = 1.0f; int travel = int(float(petTravelTargetPos - petTravelStartPos) * progress); int currentPos = petTravelStartPos + travel; if (petTravelSoundHandle != -1) { p.update_sound_1d(petTravelSoundHandle, currentPos); } if (elapsed < petTravelDurationMs) return; safe_destroy_sound(petTravelSoundHandle); petTravelSoundHandle = -1; 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 (hit) { queue_pet_event("Your " + petType + " attacks the " + petTravelTargetLabel + ".", petTravelTargetPos); } adjust_pet_loyalty(-PET_LOYALTY_ACTION_COST); } else if (petTravelAction == PET_TRAVEL_RETRIEVE) { WorldDrop@ drop = get_drop_at(petTravelTargetPos); if (drop !is null) { string message = ""; if (try_pet_pickup_world_drop(drop, message)) { remove_drop_at(drop.position); queue_pet_event(message); petOut = false; } } adjust_pet_loyalty(-PET_LOYALTY_ACTION_COST); } stop_pet_travel(); } bool run_pet_offer_menu(const string&in soundPath, const string&in reasonText) { if (petActive) return false; if (soundPath == "" || !file_exists(soundPath)) return false; if (!has_pet_food_available()) return false; string petName = get_pet_name_from_sound_path(soundPath); string prompt = "A friendly looking " + petName + " begs for food. Accept?"; if (reasonText != "") { prompt += " " + reasonText; } string[] options; options.insert_last("Yes"); options.insert_last("No"); int selection = 0; speak_with_history(prompt + " " + options[selection], true); while (true) { wait(5); if (menu_background_tick()) { return false; } if (key_pressed(KEY_ESCAPE)) { speak_with_history("Declined.", true); return false; } if (key_pressed(KEY_DOWN)) { play_menu_move_sound(); selection++; if (selection >= int(options.length())) selection = 0; speak_with_history(options[selection], true); } if (key_pressed(KEY_UP)) { play_menu_move_sound(); selection--; if (selection < 0) selection = int(options.length()) - 1; speak_with_history(options[selection], true); } if (key_pressed(KEY_RETURN)) { play_menu_select_sound(); if (selection == 0) { adopt_pet(soundPath); return true; } speak_with_history("Declined.", true); return false; } } return false; } void attempt_pet_offer_random(const string&in reasonText) { if (petActive) return; if (!has_pet_food_available()) return; init_pet_sounds(); if (petSoundPaths.length() == 0) return; int pick = random(0, petSoundPaths.length() - 1); run_pet_offer_menu(petSoundPaths[pick], reasonText); } void attempt_pet_offer_from_quest(int score) { if (score < QUEST_LOG_SCORE) return; attempt_pet_offer_random(""); } void attempt_pet_offer_from_adventure() { if (petActive) return; if (random(1, 100) > PET_ADVENTURE_CHANCE) return; attempt_pet_offer_random(""); } void attempt_pet_offer_from_tree() { if (petActive) return; if (random(1, 100) > PET_TREE_HAWK_CHANCE) return; string hawkSound = get_pet_sound_for_name("hawk"); if (hawkSound == "") return; run_pet_offer_menu(hawkSound, ""); } 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; } add_personal_count(ITEM_SMALL_GAME, 1); personal_small_game_types.insert_last(gameType); message = "Your " + petType + " retrieved " + gameType + "."; return true; } bool try_pet_pickup_world_drop(WorldDrop@ drop, string &out message) { if (drop is null) return false; if (get_flying_creature_config_by_drop_type(drop.type) !is null) { return try_pet_pickup_small_game(drop.type, message); } if (drop.type == "arrow") { int maxArrows = get_arrow_limit(); if (maxArrows <= 0) { return false; } if (get_personal_count(ITEM_ARROWS) >= maxArrows) { return false; } add_personal_count(ITEM_ARROWS, 1); message = "Your " + petType + " retrieved an arrow."; return true; } if (drop.type == "boar carcass") { if (get_personal_count(ITEM_BOAR_CARCASSES) >= get_personal_stack_limit()) { return false; } add_personal_count(ITEM_BOAR_CARCASSES, 1); message = "Your " + petType + " retrieved a boar carcass."; return true; } return false; } WorldDrop@ find_pet_drop_target() { int bestDistance = PET_RANGE + 1; WorldDrop@ best = null; for (uint i = 0; i < world_drops.length(); i++) { int distance = abs(world_drops[i].position - x); if (distance > PET_RANGE) continue; if (distance < bestDistance) { bestDistance = distance; @best = world_drops[i]; } } return best; } void update_pet_retrieval() { if (!petActive) return; if (!petOut) return; if (petLoyalty <= 0) return; if (petRetrieveTimer.elapsed < PET_RETRIEVE_COOLDOWN) return; if (petEventMessages.length() > 2) return; if (petTravelActive) return; WorldDrop@ drop = find_pet_drop_target(); if (drop is null) return; petRetrieveTimer.restart(); start_pet_travel_retrieve(drop.position); } bool find_pet_attack_target(int &out targetPos, string &out targetLabel, int &out targetKind) { int bestDistance = PET_RANGE + 1; targetPos = -1; targetLabel = ""; targetKind = -1; for (uint i = 0; i < bandits.length(); i++) { int distance = abs(bandits[i].position - x); if (distance > PET_RANGE) continue; if (distance < bestDistance) { bestDistance = distance; targetPos = bandits[i].position; targetLabel = "bandit"; targetKind = 0; } } for (uint i = 0; i < undeads.length(); i++) { if (undeads[i].undead_type == "undead_resident") continue; int distance = abs(undeads[i].position - x); if (distance > PET_RANGE) continue; if (distance < bestDistance) { bestDistance = distance; targetPos = undeads[i].position; targetLabel = "undead"; targetKind = 1; } } for (uint i = 0; i < ground_games.length(); i++) { int distance = abs(ground_games[i].position - x); if (distance > PET_RANGE) continue; if (distance < bestDistance) { bestDistance = distance; targetPos = ground_games[i].position; targetLabel = "boar"; targetKind = 2; } } return targetPos != -1; } void update_pet_attack() { if (!petActive) return; if (!petOut) return; if (petLoyalty <= 0) return; if (petAttackTimer.elapsed < PET_ATTACK_COOLDOWN) return; if (petTravelActive) return; int targetPos = -1; string targetLabel = ""; int targetKind = -1; if (!find_pet_attack_target(targetPos, targetLabel, targetKind)) return; petAttackTimer.restart(); start_pet_travel_attack(targetPos, targetLabel, targetKind); } void attempt_pet_random_find() { if (!petActive) return; if (!petOut) return; if (petLoyalty < PET_LOYALTY_BONUS_THRESHOLD) return; if (random(1, 100) > PET_RANDOM_FIND_CHANCE) return; int[] possibleItems = {ITEM_STICKS, ITEM_VINES, ITEM_STONES, ITEM_CLAY}; int itemType = possibleItems[random(0, possibleItems.length() - 1)]; int added = add_to_stack(get_personal_count(itemType), 1); if (added <= 0) return; 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); adjust_pet_loyalty(-PET_LOYALTY_ACTION_COST); petOut = false; } void handle_pet_hourly_update(int hour) { if (!petActive) return; if (get_pet_food_personal_total() == 0) { adjust_pet_loyalty(-PET_LOYALTY_HUNGER_LOSS); } if (!petActive) return; if (hour % 8 == 0) { if (consume_pet_food()) { petLoyalty += PET_LOYALTY_EAT_BONUS; clamp_pet_loyalty(); } } attempt_pet_random_find(); } void update_pets() { update_pet_travel(); if (petActive && petOut) { update_pet_retrieval(); update_pet_attack(); } update_pet_events(); }