Updates to horses and residents. Needs testing.

This commit is contained in:
Storm Dragon
2026-02-15 13:25:19 -05:00
parent c6e3036ced
commit 2606decb56
12 changed files with 338 additions and 59 deletions

View File

@@ -134,25 +134,31 @@ Pets are optional companions that can fight and retrieve drops
### Calling and status ### Calling and status
- **Space** call your pet - **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 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 - If your pet is knocked out Space reports remaining recovery time
- Press H to hear pet health and loyalty when you have a pet - Press H to hear pet health and loyalty when you have a pet
### Loyalty and feeding ### Loyalty and feeding
- Loyalty starts at 5 and max is 10 - Loyalty starts at 5 and max is 10
- If no food is available loyalty drops by 2 each hour - While your pet is out loyalty drops by 1 each hour
- Every 8 hours your pet eats 1 food and gains 2 loyalty if food is available - 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 can be meat smoked fish raw fish or basket of fruits and nuts
- Food is taken from your inventory first then storage - Food is taken from your inventory first then storage
- If loyalty reaches 0 your pet leaves
### Help in the field ### Help in the field
- When out your pet attacks bandits undead and boars within range - When out your pet attacks bandits undead and boars within range
- Each successful hit costs the pet 1 health - 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 - 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 - 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 - 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 ### Random finds
- If loyalty is at least 5 your pet can bring back sticks vines stones or clay - If loyalty is at least 5 your pet can bring back sticks vines stones or clay

BIN
sounds/pets/black_cat.ogg LFS Normal file

Binary file not shown.

BIN
sounds/terrain/fly.ogg LFS Normal file

Binary file not shown.

View File

@@ -44,13 +44,15 @@ int get_horse_success_bonus() {
return count * HORSE_SUCCESS_BONUS_PER; 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_stables.length() == 0) return 0;
if (world_storages.length() == 0) return 0; if (world_storages.length() == 0) return 0;
if (horses_count <= 0) return 0; if (horses_count <= 0) return 0;
int count = horses_count; int count = horses_count;
if (count > MAX_HORSES) count = MAX_HORSES; 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) { int get_resident_success_chance(int base_chance) {
@@ -80,12 +82,19 @@ int get_resident_cooldown(int base_cooldown) {
return 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 apply_resident_damage_bonus(int damage) {
int adjusted = damage; int adjusted = damage;
if (blessing_resident_active) { if (blessing_resident_active) {
adjusted *= get_resident_effect_multiplier(); adjusted *= get_resident_effect_multiplier();
} }
adjusted += get_horse_damage_bonus();
return adjusted; return adjusted;
} }
@@ -258,22 +267,43 @@ void attempt_livestock_production() {
void keep_base_fires_fed() { void keep_base_fires_fed() {
if (residents_count <= 0) return; if (residents_count <= 0) return;
if (!has_any_storage_food()) return; if (get_storage_count(ITEM_VINES) <= 0 && get_storage_count(ITEM_STICKS) <= 0 && get_storage_count(ITEM_LOGS) <= 0) return;
if (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++) { for (uint i = 0; i < world_fires.length(); i++) {
if (world_fires[i].position > BASE_END) continue; if (world_fires[i].position > BASE_END) continue;
if (!world_fires[i].is_burning()) 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) { while (world_fires[i].fuel_remaining < fire_target_ms) {
add_storage_count(ITEM_STICKS, -1); int needed = fire_target_ms - world_fires[i].fuel_remaining;
world_fires[i].add_fuel(300000); // 5 minutes
} else if (get_storage_count(ITEM_LOGS) > 0) { if (needed >= log_fuel_ms && get_storage_count(ITEM_LOGS) > 0) {
add_storage_count(ITEM_LOGS, -1); add_storage_count(ITEM_LOGS, -1);
world_fires[i].add_fuel(720000); // 12 minutes 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; 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 (!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) // Choose weapon type (bows preferred, otherwise weighted by availability)
int weapon_type = choose_defense_weapon_type(); int weapon_type = choose_defense_weapon_type();
@@ -389,9 +422,12 @@ int perform_resident_defense() {
int damage = 0; int damage = 0;
if (weapon_type == RESIDENT_WEAPON_BOW && bowCount > 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); 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/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) { } else if (weapon_type == RESIDENT_WEAPON_SPEAR && spearCount > 0) {
damage = apply_resident_damage_bonus(RESIDENT_SPEAR_DAMAGE); damage = apply_resident_damage_bonus(RESIDENT_SPEAR_DAMAGE);
// Weapons don't get consumed on use - they break via daily breakage check // 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); 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; return damage;
} }
// Proactive resident ranged defense // Proactive resident ranged defense
timer resident_ranged_timer;
void attempt_resident_ranged_defense() { void attempt_resident_ranged_defense() {
// Only if residents exist and have ranged weapons // Only if residents exist and have ranged weapons
if (residents_count <= 0) return; 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); bool has_sling = (slingCount > 0 && get_storage_count(ITEM_STONES) > 0);
if (!has_bow && !has_sling) return; if (!has_bow && !has_sling) return;
// Cooldown between shots // Shared cooldown for all resident combat actions
if (resident_ranged_timer.elapsed < get_resident_cooldown(RESIDENT_SLING_COOLDOWN)) return; if (resident_combat_timer.elapsed < get_resident_combat_cooldown()) return;
int range = has_bow ? BOW_RANGE : SLING_RANGE; int range = has_bow ? BOW_RANGE : SLING_RANGE;
// Find nearest enemy within range // Find nearest enemy within range
@@ -454,13 +492,18 @@ void attempt_resident_ranged_defense() {
if (targetPos == -1) return; if (targetPos == -1) return;
// Shoot! // Shoot!
resident_ranged_timer.restart(); resident_combat_timer.restart();
int damage = apply_resident_damage_bonus(random(RESIDENT_SLING_DAMAGE_MIN, RESIDENT_SLING_DAMAGE_MAX)); int damage = 0;
if (has_bow) { if (has_bow) {
damage = apply_resident_damage_bonus(random(RESIDENT_BOW_DAMAGE_MIN, RESIDENT_BOW_DAMAGE_MAX));
add_storage_count(ITEM_ARROWS, -1); 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/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); 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 { } else {
damage = apply_resident_damage_bonus(random(RESIDENT_SLING_DAMAGE_MIN, RESIDENT_SLING_DAMAGE_MAX));
add_storage_count(ITEM_STONES, -1); add_storage_count(ITEM_STONES, -1);
play_1d_with_volume_step("sounds/weapons/sling_hit.ogg", x, targetPos, false, RESIDENT_DEFENSE_VOLUME_STEP); play_1d_with_volume_step("sounds/weapons/sling_hit.ogg", x, targetPos, false, RESIDENT_DEFENSE_VOLUME_STEP);
} }

View File

@@ -29,8 +29,11 @@ const int BLESSING_HEAL_AMOUNT = 3;
const int BLESSING_BARRICADE_REPAIR = 20; const int BLESSING_BARRICADE_REPAIR = 20;
const int BLESSING_SPEED_DURATION = 300000; const int BLESSING_SPEED_DURATION = 300000;
const int BLESSING_RESIDENT_DURATION = 300000; const int BLESSING_RESIDENT_DURATION = 300000;
const int BLESSING_SEARCH_DURATION = 300000;
const int BLESSING_TRIGGER_CHANCE = 10; const int BLESSING_TRIGGER_CHANCE = 10;
const int BLESSING_WALK_SPEED = 320; 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_MIN = 1;
const int FISH_WEIGHT_MAX = 30; const int FISH_WEIGHT_MAX = 30;
// Player sex constants // Player sex constants
@@ -153,7 +156,6 @@ const int MAX_LIVESTOCK = 9;
const int HORSE_ADVENTURE_CHANCE = 45; const int HORSE_ADVENTURE_CHANCE = 45;
const int LIVESTOCK_ADVENTURE_CHANCE = 45; const int LIVESTOCK_ADVENTURE_CHANCE = 45;
const int HORSE_SUCCESS_BONUS_PER = 2; const int HORSE_SUCCESS_BONUS_PER = 2;
const int HORSE_DAMAGE_BONUS_STEP = 3;
const int LIVESTOCK_MEAT_CHANCE = 6; const int LIVESTOCK_MEAT_CHANCE = 6;
const int LIVESTOCK_SKIN_CHANCE = 3; const int LIVESTOCK_SKIN_CHANCE = 3;
const int LIVESTOCK_FEATHER_CHANCE = 2; 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_SPEAR_DAMAGE = 2;
const int RESIDENT_SLING_DAMAGE_MIN = 3; const int RESIDENT_SLING_DAMAGE_MIN = 3;
const int RESIDENT_SLING_DAMAGE_MAX = 5; 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_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_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 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; const int FALL_DAMAGE_MAX = 4;
// Base Automation // 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_COLLECTION_CHANCE = 10; // 10% chance per basket per hour
const int RESIDENT_FORAGING_CHANCE = 50; // 50% chance per resident per attempt (daily) const int RESIDENT_FORAGING_CHANCE = 50; // 50% chance per resident per attempt (daily)

View File

@@ -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); 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 count_bandits_in_range(int range_start, int range_end) {
int start = range_start; int start = range_start;
int end = range_end; 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") { void spawn_bandit(int expansion_start, int expansion_end, const string&in invader_type = "bandit") {
int spawn_x = -1; int spawn_x = -1;
if (invasion_active) { 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) { if (spawn_x == -1) {
spawn_x = pick_bandit_spawn_position(expansion_start, expansion_end); 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 // Resident defense counter-attack
if (can_residents_defend()) { if (can_residents_defend()) {
int counterDamage = perform_resident_defense(); int counterDamage = perform_resident_defense(bandit.position);
if (counterDamage > 0) { if (counterDamage > 0) {
int before_health = bandit.health; int before_health = bandit.health;
damage_bandit_at(bandit.position, counterDamage); damage_bandit_at(bandit.position, counterDamage);

View File

@@ -269,7 +269,7 @@ void try_attack_barricade_undead(Undead@ undead) {
// Resident defense counter-attack // Resident defense counter-attack
if (can_residents_defend()) { if (can_residents_defend()) {
int counterDamage = perform_resident_defense(); int counterDamage = perform_resident_defense(undead.position);
if (counterDamage > 0) { if (counterDamage > 0) {
int before_health = undead.health; int before_health = undead.health;
damage_undead_at(undead.position, counterDamage); damage_undead_at(undead.position, counterDamage);

View File

@@ -6,7 +6,9 @@ string petSoundPath = "";
string petType = ""; string petType = "";
string petGender = ""; string petGender = "";
int petLoyalty = 0; int petLoyalty = 0;
bool petHungryWarned = false;
bool petOut = false; bool petOut = false;
bool petRecallRequested = false;
int petPosition = 0; int petPosition = 0;
bool petPositionValid = false; bool petPositionValid = false;
int petHealth = 0; int petHealth = 0;
@@ -23,6 +25,7 @@ int petEventSoundHandle = -1;
const int PET_TRAVEL_NONE = 0; const int PET_TRAVEL_NONE = 0;
const int PET_TRAVEL_ATTACK = 1; const int PET_TRAVEL_ATTACK = 1;
const int PET_TRAVEL_RETRIEVE = 2; const int PET_TRAVEL_RETRIEVE = 2;
const int PET_TRAVEL_RETURN = 3;
bool petTravelActive = false; bool petTravelActive = false;
int petTravelAction = PET_TRAVEL_NONE; int petTravelAction = PET_TRAVEL_NONE;
@@ -193,11 +196,15 @@ bool consume_pet_food() {
return false; 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 (!petActive) return;
if (message.length() == 0) return; if (message.length() == 0) return;
petEventMessages.insert_last(message); petEventMessages.insert_last(message);
petEventSounds.insert_last(petSoundPath); if (playSound) {
petEventSounds.insert_last(petSoundPath);
} else {
petEventSounds.insert_last("");
}
petEventPositions.insert_last(soundPos); petEventPositions.insert_last(soundPos);
} }
@@ -226,6 +233,44 @@ int get_pet_search_origin() {
return get_pet_listener_pos(); 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() { bool is_pet_knocked_out() {
return petKnockoutHoursRemaining > 0 || petHealth <= 0; 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."); 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() { void knock_out_pet() {
if (!petActive) return; if (!petActive) return;
petHealth = 0; petHealth = 0;
@@ -329,7 +386,9 @@ void reset_pet_state() {
petType = ""; petType = "";
petGender = ""; petGender = "";
petLoyalty = 0; petLoyalty = 0;
petHungryWarned = false;
petOut = false; petOut = false;
petRecallRequested = false;
petPosition = 0; petPosition = 0;
petPositionValid = false; petPositionValid = false;
petHealth = PET_HEALTH_MAX; petHealth = PET_HEALTH_MAX;
@@ -393,21 +452,26 @@ void check_pet_call_key() {
return; return;
} }
if (petOut) { if (petOut) {
petOut = false; petRecallRequested = true;
stop_pet_travel(); play_pet_recall_ack();
petPositionValid = false; speak_with_history("Your pet is on its way.", true);
queue_pet_return_event(); request_pet_return();
return;
}
if (petLoyalty <= 2) {
speak_with_history("A " + get_pet_display_name() + " is hungry and unresponsive.", true);
return; return;
} }
adjust_pet_loyalty(-PET_LOYALTY_CALLOUT_COST); adjust_pet_loyalty(-PET_LOYALTY_CALLOUT_COST);
if (!petActive) return; if (!petActive) return;
petOut = true; petOut = true;
petRecallRequested = false;
petPosition = get_pet_listener_pos(); petPosition = get_pet_listener_pos();
petPositionValid = true; 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. /* But I can feel it, black water washes over me.
As it soothes I call to you with my control. */ 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); petType = get_pet_name_from_sound_path(soundPath);
petGender = (random(0, 1) == 0) ? "Male" : "Female"; petGender = (random(0, 1) == 0) ? "Male" : "Female";
petLoyalty = PET_START_LOYALTY; petLoyalty = PET_START_LOYALTY;
petHungryWarned = false;
petOut = false; petOut = false;
petRecallRequested = false;
petPosition = 0; petPosition = 0;
petPositionValid = false; petPositionValid = false;
petHealth = PET_HEALTH_MAX; petHealth = PET_HEALTH_MAX;
@@ -452,6 +518,8 @@ void start_pet_travel_attack(int targetPos, const string&in targetLabel, int tar
petTravelActive = true; petTravelActive = true;
petTravelAction = PET_TRAVEL_ATTACK; petTravelAction = PET_TRAVEL_ATTACK;
petTravelStartPos = petPositionValid ? petPosition : get_pet_listener_pos(); petTravelStartPos = petPositionValid ? petPosition : get_pet_listener_pos();
petPosition = petTravelStartPos;
petPositionValid = true;
petTravelTargetPos = targetPos; petTravelTargetPos = targetPos;
petTravelTargetLabel = targetLabel; petTravelTargetLabel = targetLabel;
petTravelTargetKind = targetKind; petTravelTargetKind = targetKind;
@@ -474,11 +542,63 @@ void start_pet_travel_retrieve(int targetPos) {
petTravelActive = true; petTravelActive = true;
petTravelAction = PET_TRAVEL_RETRIEVE; petTravelAction = PET_TRAVEL_RETRIEVE;
petTravelStartPos = petPositionValid ? petPosition : get_pet_listener_pos(); petTravelStartPos = petPositionValid ? petPosition : get_pet_listener_pos();
petPosition = petTravelStartPos;
petPositionValid = true;
petTravelTargetPos = targetPos; petTravelTargetPos = targetPos;
petTravelDurationMs = get_pet_travel_duration_ms(petTravelStartPos, targetPos); petTravelDurationMs = get_pet_travel_duration_ms(petTravelStartPos, targetPos);
petTravelTimer.restart(); 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() { void update_pet_travel() {
if (!petTravelActive) return; if (!petTravelActive) return;
if (petTravelDurationMs < 1) petTravelDurationMs = 1; if (petTravelDurationMs < 1) petTravelDurationMs = 1;
@@ -488,6 +608,8 @@ void update_pet_travel() {
if (find_pet_attack_target_by_kind(petTravelTargetKind, petTravelTargetPos, refreshedTargetPos)) { if (find_pet_attack_target_by_kind(petTravelTargetKind, petTravelTargetPos, refreshedTargetPos)) {
petTravelTargetPos = refreshedTargetPos; petTravelTargetPos = refreshedTargetPos;
} }
} else if (petTravelAction == PET_TRAVEL_RETURN) {
petTravelTargetPos = get_pet_listener_pos();
} }
int elapsed = petTravelTimer.elapsed; int elapsed = petTravelTimer.elapsed;
@@ -496,6 +618,7 @@ void update_pet_travel() {
int travel = int(float(petTravelTargetPos - petTravelStartPos) * progress); int travel = int(float(petTravelTargetPos - petTravelStartPos) * progress);
int currentPos = petTravelStartPos + travel; int currentPos = petTravelStartPos + travel;
update_pet_travel_position(currentPos);
if (petTravelSoundHandle != -1) { if (petTravelSoundHandle != -1) {
p.update_sound_1d(petTravelSoundHandle, currentPos); p.update_sound_1d(petTravelSoundHandle, currentPos);
} }
@@ -539,9 +662,7 @@ void update_pet_travel() {
start_pet_travel_retrieve(drop.position); start_pet_travel_retrieve(drop.position);
return; return;
} }
petOut = false; request_pet_follow();
petPositionValid = false;
queue_pet_return_event();
} }
} else if (petTravelAction == PET_TRAVEL_RETRIEVE) { } else if (petTravelAction == PET_TRAVEL_RETRIEVE) {
WorldDrop@ drop = get_drop_at(petTravelTargetPos); WorldDrop@ drop = get_drop_at(petTravelTargetPos);
@@ -549,7 +670,7 @@ void update_pet_travel() {
string message = ""; string message = "";
if (try_pet_pickup_world_drop(drop, message)) { if (try_pet_pickup_world_drop(drop, message)) {
remove_drop_at(drop.position); remove_drop_at(drop.position);
queue_pet_event(message); queue_pet_event(message, -1, false);
petPosition = petTravelTargetPos; petPosition = petTravelTargetPos;
petPositionValid = true; petPositionValid = true;
WorldDrop@ nextDrop = find_pet_drop_target(); WorldDrop@ nextDrop = find_pet_drop_target();
@@ -566,11 +687,22 @@ void update_pet_travel() {
start_pet_travel_attack(nextTargetPos, nextTargetLabel, nextTargetKind); start_pet_travel_attack(nextTargetPos, nextTargetLabel, nextTargetKind);
return; return;
} }
petOut = false; request_pet_follow();
petPositionValid = false;
queue_pet_return_event();
} }
} }
} 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(); stop_pet_travel();
@@ -840,6 +972,25 @@ void update_pet_attack() {
start_pet_travel_attack(targetPos, targetLabel, targetKind); 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() { void attempt_pet_random_find() {
if (!petActive) return; if (!petActive) return;
if (!petOut) return; if (!petOut) return;
@@ -853,9 +1004,8 @@ void attempt_pet_random_find() {
add_personal_count(itemType, added); add_personal_count(itemType, added);
string itemName = (added == 1) ? item_registry[itemType].singular : item_registry[itemType].name; 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); queue_pet_event("Your " + get_pet_display_name() + " retrieved " + added + " " + itemName + ".", petPositionValid ? petPosition : x, false);
petOut = false; request_pet_follow();
petPositionValid = false;
} }
void handle_pet_hourly_update(int hour) { 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; petHealth += 1;
if (petHealth > PET_HEALTH_MAX) petHealth = PET_HEALTH_MAX; if (petHealth > PET_HEALTH_MAX) petHealth = PET_HEALTH_MAX;
} }
if (!has_pet_food_available()) { if (petOut) {
adjust_pet_loyalty(-PET_LOYALTY_HUNGER_LOSS); 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 && !petOut) {
if (hour % 8 == 0) {
if (consume_pet_food()) { if (consume_pet_food()) {
petLoyalty += PET_LOYALTY_EAT_BONUS; petLoyalty = PET_LOYALTY_MAX;
clamp_pet_loyalty(); if (petLoyalty > 1) {
petHungryWarned = false;
}
} }
} }
@@ -896,6 +1051,7 @@ void update_pets() {
if (petActive && petOut) { if (petActive && petOut) {
update_pet_retrieval(); update_pet_retrieval();
update_pet_attack(); update_pet_attack();
update_pet_follow();
} }
update_pet_events(); update_pet_events();
} }

View File

@@ -80,6 +80,8 @@ bool blessing_speed_active = false;
timer blessing_speed_timer; timer blessing_speed_timer;
bool blessing_resident_active = false; bool blessing_resident_active = false;
timer blessing_resident_timer; timer blessing_resident_timer;
bool blessing_search_active = false;
timer blessing_search_timer;
// Timers // Timers
timer walktimer; timer walktimer;

View File

@@ -36,14 +36,17 @@ int get_total_rune_gather_bonus() {
return 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 // Takes base time in ms, returns reduced time
int apply_rune_gather_bonus(int base_time) { int apply_rune_gather_bonus(int base_time) {
int bonus_percent = get_total_rune_gather_bonus(); 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; if (bonus_percent <= 0) return base_time;
// Cap at 50% reduction to prevent instant gathering // Keep gathering from becoming instant.
if (bonus_percent > 50) bonus_percent = 50; if (bonus_percent > GATHER_TIME_REDUCTION_CAP) bonus_percent = GATHER_TIME_REDUCTION_CAP;
int reduction = (base_time * bonus_percent) / 100; int reduction = (base_time * bonus_percent) / 100;
return base_time - reduction; return base_time - reduction;

View File

@@ -623,6 +623,7 @@ void reset_game_state() {
incense_burning = false; incense_burning = false;
blessing_speed_active = false; blessing_speed_active = false;
blessing_resident_active = false; blessing_resident_active = false;
blessing_search_active = false;
reset_fylgja_state(); reset_fylgja_state();
reset_pet_state(); reset_pet_state();

View File

@@ -662,6 +662,10 @@ void update_blessings() {
blessing_resident_active = false; blessing_resident_active = false;
speak_with_history("The residents' purpose fades.", true); 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() { void attempt_blessing() {
@@ -674,6 +678,7 @@ void attempt_blessing() {
if (!blessing_speed_active) options.insert_last(1); if (!blessing_speed_active) options.insert_last(1);
if (barricade_health < BARRICADE_MAX_HEALTH) options.insert_last(2); if (barricade_health < BARRICADE_MAX_HEALTH) options.insert_last(2);
if (residents_count > 0 && !blessing_resident_active) options.insert_last(3); if (residents_count > 0 && !blessing_resident_active) options.insert_last(3);
if (!blessing_search_active) options.insert_last(4);
if (options.length() == 0) return; if (options.length() == 0) return;
int choice = options[random(0, options.length() - 1)]; int choice = options[random(0, options.length() - 1)];
@@ -708,6 +713,10 @@ void attempt_blessing() {
blessing_resident_active = true; blessing_resident_active = true;
blessing_resident_timer.restart(); blessing_resident_timer.restart();
notify(god_name + " radiance fills residents with purpose."); 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.");
} }
} }