// Base automation helpers int get_food_requirement() { if (residents_count <= 0) return 0; return residents_count; // 1 food per resident per 8 hours } int get_total_storage_food() { return get_storage_count(ITEM_MEAT) + get_storage_count(ITEM_SMOKED_FISH) + get_storage_count(ITEM_BASKET_FOOD); } bool has_any_storage_food() { return get_total_storage_food() > 0; } bool has_any_streams() { if (world_streams.length() > 0) return true; for (uint i = 0; i < world_mountains.length(); i++) { if (world_mountains[i].stream_positions.length() > 0) { return true; } } return false; } bool has_burning_fire_in_base() { for (uint i = 0; i < world_fires.length(); i++) { if (world_fires[i].position <= BASE_END && world_fires[i].is_burning()) { return true; } } return false; } int get_resident_effect_multiplier() { return blessing_resident_active ? 2 : 1; } int get_horse_success_bonus() { 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_SUCCESS_BONUS_PER; } 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; 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 chance = base_chance * get_resident_effect_multiplier(); chance += get_horse_success_bonus(); if (chance > 100) chance = 100; return chance; } int get_resident_break_chance(int base_chance) { if (!blessing_resident_active) return base_chance; int reduced = base_chance / get_resident_effect_multiplier(); if (reduced < 1 && base_chance > 0) reduced = 1; return reduced; } int get_resident_escape_chance(int base_chance) { if (!blessing_resident_active) return base_chance; int reduced = base_chance / get_resident_effect_multiplier(); if (reduced < 1 && base_chance > 0) reduced = 1; return reduced; } int get_resident_cooldown(int base_cooldown) { int cooldown = base_cooldown / get_resident_effect_multiplier(); if (cooldown < 1) cooldown = 1; 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(); } return adjusted; } void consume_food_for_residents() { int needed = get_food_requirement(); if (needed <= 0) return; int meat_available = get_storage_count(ITEM_MEAT); int smoked_fish_available = get_storage_count(ITEM_SMOKED_FISH); int basket_food_available = get_storage_count(ITEM_BASKET_FOOD); int total_food = meat_available + smoked_fish_available + basket_food_available; if (total_food >= needed) { // Priority: meat -> smoked fish -> basket food int from_meat = (meat_available >= needed) ? needed : meat_available; add_storage_count(ITEM_MEAT, -from_meat); int remaining = needed - from_meat; if (remaining > 0) { int from_fish = (smoked_fish_available >= remaining) ? remaining : smoked_fish_available; add_storage_count(ITEM_SMOKED_FISH, -from_fish); remaining -= from_fish; } if (remaining > 0) { add_storage_count(ITEM_BASKET_FOOD, -remaining); } } else { set_storage_count(ITEM_MEAT, 0); set_storage_count(ITEM_SMOKED_FISH, 0); set_storage_count(ITEM_BASKET_FOOD, 0); if (x <= BASE_END) { notify(tr("system.base.no_food_residents_hungry")); } } } void attempt_resident_fishing() { if (!is_daytime) return; if (residents_count <= 0) return; if (get_storage_count(ITEM_FISHING_POLES) <= 0) return; if (!has_any_streams()) return; if (get_storage_count(ITEM_FISH) >= get_storage_stack_limit()) return; int active_fishers = residents_count; int poles = get_storage_count(ITEM_FISHING_POLES); if (poles < active_fishers) active_fishers = poles; int caught = 0; int poles_broken = 0; int break_chance = get_resident_break_chance(RESIDENT_TOOL_BREAK_CHANCE); int fishing_chance = get_resident_success_chance(RESIDENT_FISHING_CHANCE); for (int i = 0; i < active_fishers; i++) { // Check for pole breakage during use if (random(1, 100) <= break_chance) { poles_broken++; continue; } if (random(1, 100) > fishing_chance) continue; if (get_storage_count(ITEM_FISH) >= get_storage_stack_limit()) break; add_storage_count(ITEM_FISH, 1); add_storage_fish_weight(random(FISH_WEIGHT_MIN, FISH_WEIGHT_MAX)); caught++; } // Apply pole breakage if (poles_broken > 0) { add_storage_count(ITEM_FISHING_POLES, -poles_broken); if (x <= BASE_END) { string msg = tr("system.base.resident_fishing_pole_broke_one"); if (poles_broken > 1) { dictionary polesBrokenArgs; polesBrokenArgs.set("count", poles_broken); msg = trf("system.base.resident_fishing_pole_broke_many", polesBrokenArgs); } speak_with_history(msg, true); } } if (caught > 0 && x <= BASE_END) { if (caught == 1) { speak_with_history(tr("system.base.resident_caught_fish_one"), true); } else { dictionary caughtFishArgs; caughtFishArgs.set("count", caught); speak_with_history(trf("system.base.resident_caught_fish_many", caughtFishArgs), true); } } } void attempt_resident_fish_smoking() { if (!is_daytime) return; if (residents_count <= 0) return; if (get_storage_count(ITEM_FISH) <= 0) return; if (get_storage_count(ITEM_STICKS) <= 0) return; if (!has_burning_fire_in_base()) return; int attempts = get_resident_effect_multiplier(); int smoke_chance = get_resident_success_chance(RESIDENT_SMOKE_FISH_CHANCE); for (int attempt = 0; attempt < attempts; attempt++) { if (get_storage_count(ITEM_FISH) <= 0) return; if (get_storage_count(ITEM_STICKS) <= 0) return; if (random(1, 100) > smoke_chance) continue; int weight = (storage_fish_weights.length() > 0) ? storage_fish_weights[0] : get_default_fish_weight(); int yield = get_smoked_fish_yield(weight); if (get_storage_count(ITEM_SMOKED_FISH) + yield > get_storage_stack_limit()) return; pop_storage_fish_weight(); add_storage_count(ITEM_FISH, -1); add_storage_count(ITEM_STICKS, -1); add_storage_count(ITEM_SMOKED_FISH, yield); if (x <= BASE_END) { dictionary smokedFishArgs; smokedFishArgs.set("yield", yield); speak_with_history(trf("system.base.resident_smoked_fish", smokedFishArgs), true); } } } void attempt_livestock_production() { if (world_pastures.length() == 0) return; if (world_storages.length() == 0) return; if (livestock_count <= 0) return; int count = livestock_count; if (count > MAX_LIVESTOCK) count = MAX_LIVESTOCK; int meat_produced = 0; int skins_produced = 0; int feathers_produced = 0; for (int i = 0; i < count; i++) { if (get_storage_count(ITEM_MEAT) < get_storage_stack_limit()) { if (random(1, 100) <= LIVESTOCK_MEAT_CHANCE) { add_storage_count(ITEM_MEAT, 1); meat_produced++; } } if (get_storage_count(ITEM_SKINS) < get_storage_stack_limit()) { if (random(1, 100) <= LIVESTOCK_SKIN_CHANCE) { add_storage_count(ITEM_SKINS, 1); skins_produced++; } } if (get_storage_count(ITEM_FEATHERS) < get_storage_stack_limit()) { if (random(1, 100) <= LIVESTOCK_FEATHER_CHANCE) { add_storage_count(ITEM_FEATHERS, 1); feathers_produced++; } } } if ((meat_produced > 0 || skins_produced > 0 || feathers_produced > 0) && x <= BASE_END) { dictionary livestockArgs; livestockArgs.set("meat", meat_produced); livestockArgs.set("skins", skins_produced); livestockArgs.set("feathers", feathers_produced); speak_with_history(trf("system.base.livestock_produced", livestockArgs), true); } } void keep_base_fires_fed() { if (residents_count <= 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 >= fire_target_ms) continue; 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; } } } } // Resident defense functions const int RESIDENT_WEAPON_SPEAR = 0; const int RESIDENT_WEAPON_SLING = 1; const int RESIDENT_WEAPON_BOW = 2; int get_stored_runed_weapon_count(int equipType) { int total = 0; int[] runeTypes; get_all_rune_types(runeTypes); for (uint i = 0; i < runeTypes.length(); i++) { total += get_stored_runed_item_count(equipType, runeTypes[i]); } return total; } int get_total_stored_weapon_count(int equipType, int itemType) { return get_storage_count(itemType) + get_stored_runed_weapon_count(equipType); } bool remove_random_stored_runed_weapon(int equipType) { int[] runeTypes; get_all_rune_types(runeTypes); int total = 0; for (uint i = 0; i < runeTypes.length(); i++) { total += get_stored_runed_item_count(equipType, runeTypes[i]); } if (total <= 0) return false; int roll = random(1, total); int running = 0; for (uint i = 0; i < runeTypes.length(); i++) { int count = get_stored_runed_item_count(equipType, runeTypes[i]); if (count <= 0) continue; running += count; if (roll <= running) { remove_stored_runed_item(equipType, runeTypes[i]); return true; } } return false; } bool remove_random_stored_weapon(int equipType, int itemType) { int unrunedCount = get_storage_count(itemType); int runedCount = get_stored_runed_weapon_count(equipType); int total = unrunedCount + runedCount; if (total <= 0) return false; int roll = random(1, total); if (roll <= unrunedCount) { if (unrunedCount > 0) add_storage_count(itemType, -1); return true; } return remove_random_stored_runed_weapon(equipType); } int get_available_defense_weapons() { int spearCount = get_total_stored_weapon_count(EQUIP_SPEAR, ITEM_SPEARS); int slingCount = 0; int bowCount = 0; // Slings only count if stones are available if (get_storage_count(ITEM_STONES) > 0) { slingCount = get_total_stored_weapon_count(EQUIP_SLING, ITEM_SLINGS); } // Bows only count if arrows are available if (get_storage_count(ITEM_ARROWS) > 0) { bowCount = get_total_stored_weapon_count(EQUIP_BOW, ITEM_BOWS); } return spearCount + slingCount + bowCount; } bool can_residents_defend() { if (residents_count <= 0) return false; return get_available_defense_weapons() > 0; } int choose_defense_weapon_type() { // Prefer bows if available int bowCount = (get_storage_count(ITEM_ARROWS) > 0) ? get_total_stored_weapon_count(EQUIP_BOW, ITEM_BOWS) : 0; if (bowCount > 0) return RESIDENT_WEAPON_BOW; int spearCount = get_total_stored_weapon_count(EQUIP_SPEAR, ITEM_SPEARS); int slingCount = (get_storage_count(ITEM_STONES) > 0) ? get_total_stored_weapon_count(EQUIP_SLING, ITEM_SLINGS) : 0; int total = spearCount + slingCount; if (total == 0) return RESIDENT_WEAPON_SPEAR; if (slingCount == 0) return RESIDENT_WEAPON_SPEAR; if (spearCount == 0) return RESIDENT_WEAPON_SLING; int roll = random(1, total); return (roll <= spearCount) ? RESIDENT_WEAPON_SPEAR : RESIDENT_WEAPON_SLING; } 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(); int bowCount = (get_storage_count(ITEM_ARROWS) > 0) ? get_total_stored_weapon_count(EQUIP_BOW, ITEM_BOWS) : 0; int spearCount = get_total_stored_weapon_count(EQUIP_SPEAR, ITEM_SPEARS); int slingCount = (get_storage_count(ITEM_STONES) > 0) ? get_total_stored_weapon_count(EQUIP_SLING, ITEM_SLINGS) : 0; int damage = 0; if (weapon_type == RESIDENT_WEAPON_BOW && bowCount > 0) { 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 // Just play the sound play_1d_with_volume_step("sounds/weapons/spear_swing.ogg", x, BASE_END + 1, false, RESIDENT_DEFENSE_VOLUME_STEP); } else if (weapon_type == RESIDENT_WEAPON_SLING && slingCount > 0) { damage = apply_resident_damage_bonus(random(RESIDENT_SLING_DAMAGE_MIN, RESIDENT_SLING_DAMAGE_MAX)); // Slings use stones as ammo, so consume a stone add_storage_count(ITEM_STONES, -1); 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 void attempt_resident_ranged_defense() { // Only if residents exist and have ranged weapons if (residents_count <= 0) return; int bowCount = get_total_stored_weapon_count(EQUIP_BOW, ITEM_BOWS); int slingCount = get_total_stored_weapon_count(EQUIP_SLING, ITEM_SLINGS); bool has_bow = (bowCount > 0 && get_storage_count(ITEM_ARROWS) > 0); bool has_sling = (slingCount > 0 && get_storage_count(ITEM_STONES) > 0); if (!has_bow && !has_sling) 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 int nearestDistance = range + 1; int targetPos = -1; bool targetIsBandit = false; int ranged_origin = BASE_END; // Check zombies for (uint i = 0; i < zombies.length(); i++) { int dist = abs(zombies[i].position - ranged_origin); if (dist > 0 && dist <= range && dist < nearestDistance) { nearestDistance = dist; targetPos = zombies[i].position; targetIsBandit = false; } } // Check bandits for (uint i = 0; i < bandits.length(); i++) { int dist = abs(bandits[i].position - ranged_origin); if (dist > 0 && dist <= range && dist < nearestDistance) { nearestDistance = dist; targetPos = bandits[i].position; targetIsBandit = true; } } // No targets in range if (targetPos == -1) return; // Shoot! 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); } if (targetIsBandit) { damage_bandit_at(targetPos, damage); } else { damage_zombie_at(targetPos, damage); } // Play hit sound on enemy if (targetIsBandit) { play_creature_hit_sound("sounds/enemies/zombie_hit.ogg", x, targetPos, BANDIT_SOUND_VOLUME_STEP); } else { play_creature_hit_sound("sounds/enemies/zombie_hit.ogg", x, targetPos, ZOMBIE_SOUND_VOLUME_STEP); } } void process_daily_weapon_breakage() { if (residents_count <= 0) return; int spearTotal = get_total_stored_weapon_count(EQUIP_SPEAR, ITEM_SPEARS); int slingTotal = get_total_stored_weapon_count(EQUIP_SLING, ITEM_SLINGS); int bowTotal = get_total_stored_weapon_count(EQUIP_BOW, ITEM_BOWS); int totalWeapons = spearTotal + slingTotal + bowTotal; if (totalWeapons == 0) return; // Number of breakage checks = min(residents, weapons) int checksToPerform = (residents_count < totalWeapons) ? residents_count : totalWeapons; // Distribute checks among available weapons int spearChecks = 0; int slingChecks = 0; int bowChecks = 0; for (int i = 0; i < checksToPerform; i++) { int remainingSpears = spearTotal - spearChecks; int remainingSlings = slingTotal - slingChecks; int remainingBows = bowTotal - bowChecks; int remaining = remainingSpears + remainingSlings + remainingBows; if (remaining <= 0) break; int roll = random(1, remaining); if (roll <= remainingSpears && remainingSpears > 0) { spearChecks++; } else if (roll <= remainingSpears + remainingBows && remainingBows > 0) { bowChecks++; } else if (remainingSlings > 0) { slingChecks++; } } // Perform breakage checks int spearsBroken = 0; int slingsBroken = 0; int bowsBroken = 0; int break_chance = get_resident_break_chance(RESIDENT_WEAPON_BREAK_CHANCE); for (int i = 0; i < spearChecks; i++) { if (random(1, 100) <= break_chance) { spearsBroken++; } } for (int i = 0; i < slingChecks; i++) { if (random(1, 100) <= break_chance) { slingsBroken++; } } for (int i = 0; i < bowChecks; i++) { if (random(1, 100) <= break_chance) { bowsBroken++; } } // Apply breakage if (spearsBroken > 0) { for (int i = 0; i < spearsBroken; i++) { remove_random_stored_weapon(EQUIP_SPEAR, ITEM_SPEARS); } string msg = tr("system.base.resident_spear_broke_one"); if (spearsBroken > 1) { dictionary spearsBrokenArgs; spearsBrokenArgs.set("count", spearsBroken); msg = trf("system.base.resident_spear_broke_many", spearsBrokenArgs); } notify(msg); } if (slingsBroken > 0) { for (int i = 0; i < slingsBroken; i++) { remove_random_stored_weapon(EQUIP_SLING, ITEM_SLINGS); } string msg = tr("system.base.resident_sling_broke_one"); if (slingsBroken > 1) { dictionary slingsBrokenArgs; slingsBrokenArgs.set("count", slingsBroken); msg = trf("system.base.resident_sling_broke_many", slingsBrokenArgs); } notify(msg); } if (bowsBroken > 0) { for (int i = 0; i < bowsBroken; i++) { remove_random_stored_weapon(EQUIP_BOW, ITEM_BOWS); } string msg = tr("system.base.resident_bow_broke_one"); if (bowsBroken > 1) { dictionary bowsBrokenArgs; bowsBrokenArgs.set("count", bowsBroken); msg = trf("system.base.resident_bow_broke_many", bowsBrokenArgs); } notify(msg); } } void attempt_resident_clothing_repairs() { if (residents_count <= 0) return; int threshold = get_storage_stack_limit() / 2; if (threshold < RESIDENT_CLOTHING_REPAIR_COST) { threshold = RESIDENT_CLOTHING_REPAIR_COST; } int vines_used = 0; int skins_used = 0; int down_used = 0; int repairs_done = 0; for (int i = 0; i < residents_count; i++) { int best_item = -1; int best_count = -1; int vines_count = get_storage_count(ITEM_VINES); if (vines_count >= RESIDENT_CLOTHING_REPAIR_COST && vines_count > threshold && vines_count > best_count) { best_item = ITEM_VINES; best_count = vines_count; } int skins_count = get_storage_count(ITEM_SKINS); if (skins_count >= RESIDENT_CLOTHING_REPAIR_COST && skins_count > threshold && skins_count > best_count) { best_item = ITEM_SKINS; best_count = skins_count; } int down_count = get_storage_count(ITEM_DOWN); if (down_count >= RESIDENT_CLOTHING_REPAIR_COST && down_count > threshold && down_count > best_count) { best_item = ITEM_DOWN; best_count = down_count; } if (best_item == -1) break; add_storage_count(best_item, -RESIDENT_CLOTHING_REPAIR_COST); repairs_done++; if (best_item == ITEM_VINES) { vines_used += RESIDENT_CLOTHING_REPAIR_COST; } else if (best_item == ITEM_SKINS) { skins_used += RESIDENT_CLOTHING_REPAIR_COST; } else if (best_item == ITEM_DOWN) { down_used += RESIDENT_CLOTHING_REPAIR_COST; } } if (repairs_done > 0 && x <= BASE_END) { dictionary repairArgs; repairArgs.set("vines", vines_used); repairArgs.set("skins", skins_used); repairArgs.set("down", down_used); string msg = trf("system.base.resident_clothing_repair_many", repairArgs); if (repairs_done == 1) msg = trf("system.base.resident_clothing_repair_one", repairArgs); speak_with_history(msg, true); } } // Resident snare retrieval void attempt_resident_snare_retrieval() { // Only during daytime if (!is_daytime) return; // Need residents if (residents_count <= 0) return; // Need food in storage (same limitation as other resident tasks) if (!has_any_storage_food()) return; int check_chance = get_resident_success_chance(RESIDENT_SNARE_CHECK_CHANCE); int escape_chance = get_resident_escape_chance(RESIDENT_SNARE_ESCAPE_CHANCE); // Check each snare that has a catch for (int i = int(world_snares.length()) - 1; i >= 0; i--) { WorldSnare @snare = world_snares[i]; if (!snare.has_catch) continue; if (!snare.active) continue; // Each snare has a chance to be checked by a resident this hour if (random(1, 100) > check_chance) continue; // Small chance the game escapes during retrieval (like normal) if (random(1, 100) <= escape_chance) { dictionary escapedArgs; escapedArgs.set("catch_type", i18n_translate_fragment_value(snare.catch_type)); escapedArgs.set("position", snare.position); notify(trf("system.base.snare_escape_during_check", escapedArgs)); remove_snare_at(snare.position); continue; } // Check if storage has room for small game if (get_storage_count(ITEM_SMALL_GAME) >= get_storage_stack_limit()) continue; // Retrieve the game string game_type = snare.catch_type; int pos = snare.position; // Add game to storage add_storage_count(ITEM_SMALL_GAME, 1); storage_small_game_types.insert_last(game_type); // Reset the snare in place (resident empties it and resets for more game) snare.has_catch = false; snare.catch_type = ""; snare.catch_chance = 5; snare.escape_chance = 0; snare.hours_with_catch = 0; snare.hour_timer.restart(); dictionary snareRetrievedArgs; snareRetrievedArgs.set("game_type", i18n_translate_fragment_value(game_type)); snareRetrievedArgs.set("position", pos); notify(trf("system.base.resident_retrieved_from_snare", snareRetrievedArgs)); } } // Resident butchering - processes up to residents_count games per day (doubled when blessed) void attempt_resident_butchering() { // Need residents if (residents_count <= 0) return; // Need food in storage (same limitation as other resident tasks) if (!has_any_storage_food()) return; // Need game in storage if (get_storage_count(ITEM_SMALL_GAME) <= 0 && get_storage_count(ITEM_BOAR_CARCASSES) <= 0) return; // Need a knife in storage if (get_storage_count(ITEM_KNIVES) <= 0) return; // Need a fire in base if (!has_burning_fire_in_base()) return; int attempts = residents_count * get_resident_effect_multiplier(); int break_chance = get_resident_break_chance(RESIDENT_TOOL_BREAK_CHANCE); for (int attempt = 0; attempt < attempts; attempt++) { // Need game in storage if (get_storage_count(ITEM_SMALL_GAME) <= 0 && get_storage_count(ITEM_BOAR_CARCASSES) <= 0) return; if (get_storage_count(ITEM_KNIVES) <= 0) return; // Determine what to butcher (prioritize boar carcasses) string game_type = ""; bool is_boar = false; if (get_storage_count(ITEM_BOAR_CARCASSES) > 0) { game_type = "boar carcass"; is_boar = true; } else if (storage_small_game_types.length() > 0) { game_type = storage_small_game_types[0]; } else { game_type = "small game"; } // Calculate outputs and check storage capacity int meat_yield = 0; int skins_yield = 0; int feathers_yield = 0; int down_yield = 0; int sinew_yield = 0; if (game_type == "goose") { meat_yield = 1; feathers_yield = random(3, 6); down_yield = random(1, 3); } else if (game_type == "turkey") { meat_yield = 1; feathers_yield = random(1, 4); } else if (is_boar) { meat_yield = random(2, 3); skins_yield = 3; sinew_yield = 2; } else { meat_yield = 1; skins_yield = 1; } // Check storage capacity for outputs if (meat_yield > 0 && get_storage_count(ITEM_MEAT) + meat_yield > get_storage_stack_limit()) return; if (skins_yield > 0 && get_storage_count(ITEM_SKINS) + skins_yield > get_storage_stack_limit()) return; if (feathers_yield > 0 && get_storage_count(ITEM_FEATHERS) + feathers_yield > get_storage_stack_limit()) return; if (down_yield > 0 && get_storage_count(ITEM_DOWN) + down_yield > get_storage_stack_limit()) return; if (sinew_yield > 0 && get_storage_count(ITEM_SINEW) + sinew_yield > get_storage_stack_limit()) return; // Consume the game if (is_boar) { add_storage_count(ITEM_BOAR_CARCASSES, -1); } else { add_storage_count(ITEM_SMALL_GAME, -1); if (storage_small_game_types.length() > 0) { storage_small_game_types.remove_at(0); } } // Check for knife breakage if (random(1, 100) <= break_chance) { add_storage_count(ITEM_KNIVES, -1); notify(tr("system.base.resident_knife_broke_butchering")); } // Add outputs to storage if (meat_yield > 0) add_storage_count(ITEM_MEAT, meat_yield); if (skins_yield > 0) add_storage_count(ITEM_SKINS, skins_yield); if (feathers_yield > 0) add_storage_count(ITEM_FEATHERS, feathers_yield); if (down_yield > 0) add_storage_count(ITEM_DOWN, down_yield); if (sinew_yield > 0) add_storage_count(ITEM_SINEW, sinew_yield); dictionary butcherArgs; butcherArgs.set("game_type", i18n_translate_fragment_value(game_type)); butcherArgs.set("meat", meat_yield); butcherArgs.set("skins", skins_yield); butcherArgs.set("feathers", feathers_yield); butcherArgs.set("down", down_yield); butcherArgs.set("sinew", sinew_yield); notify(trf("system.base.resident_butchered_result", butcherArgs)); } } // Resident resource collection void attempt_resident_collection() { // Only during daytime if (!is_daytime) return; // Need residents if (residents_count <= 0) return; // Need baskets in storage to enable collection if (get_storage_count(ITEM_REED_BASKETS) <= 0) return; // Number of residents who can collect = min(residents, baskets) int active_collectors = (residents_count < get_storage_count(ITEM_REED_BASKETS)) ? residents_count : get_storage_count(ITEM_REED_BASKETS); // Each active collector has a 10% chance to collect something int baskets_broken = 0; int break_chance = get_resident_break_chance(RESIDENT_TOOL_BREAK_CHANCE); int collection_chance = get_resident_success_chance(RESIDENT_COLLECTION_CHANCE); for (int i = 0; i < active_collectors; i++) { // Check for basket breakage during use if (random(1, 100) <= break_chance) { baskets_broken++; continue; } if (random(1, 100) > collection_chance) continue; // Determine what to collect (weighted random) // Sticks and vines more common, logs and stones less common int roll = random(1, 100); string item_name = ""; if (roll <= 40) { // 40% chance - stick add_storage_count(ITEM_STICKS, 1); item_name = "stick"; } else if (roll <= 70) { // 30% chance - vine add_storage_count(ITEM_VINES, 1); item_name = "vine"; } else if (roll <= 85) { // 15% chance - stone add_storage_count(ITEM_STONES, 1); item_name = "stone"; } else { // 15% chance - log add_storage_count(ITEM_LOGS, 1); item_name = "log"; } // Announce only if player is in base if (x <= BASE_END) { dictionary addedArgs; addedArgs.set("item", i18n_translate_fragment_value(item_name)); speak_with_history(trf("system.base.resident_added_to_storage", addedArgs), true); } } // Apply basket breakage if (baskets_broken > 0) { add_storage_count(ITEM_REED_BASKETS, -baskets_broken); if (x <= BASE_END) { string msg = tr("system.base.resident_basket_broke_one"); if (baskets_broken > 1) { dictionary basketBrokenArgs; basketBrokenArgs.set("count", baskets_broken); msg = trf("system.base.resident_basket_broke_many", basketBrokenArgs); } speak_with_history(msg, true); } } } // Resident foraging - produces baskets of fruits and nuts from reed baskets void attempt_resident_foraging() { // Only during daytime if (!is_daytime) return; // Need residents if (residents_count <= 0) return; // Need reed baskets in storage if (get_storage_count(ITEM_REED_BASKETS) <= 0) return; // Check if storage has room for basket food if (get_storage_count(ITEM_BASKET_FOOD) >= get_storage_stack_limit()) return; // Number of residents who can forage = min(residents, baskets) int active_foragers = (residents_count < get_storage_count(ITEM_REED_BASKETS)) ? residents_count : get_storage_count(ITEM_REED_BASKETS); int baskets_produced = 0; int baskets_broken = 0; int break_chance = get_resident_break_chance(RESIDENT_TOOL_BREAK_CHANCE); int forage_chance = get_resident_success_chance(RESIDENT_FORAGING_CHANCE); for (int i = 0; i < active_foragers; i++) { // Check for basket breakage during foraging if (random(1, 100) <= break_chance) { baskets_broken++; continue; } // Check if foraging succeeds if (random(1, 100) > forage_chance) continue; // Check storage capacity if (get_storage_count(ITEM_BASKET_FOOD) >= get_storage_stack_limit()) break; // Consume a reed basket and produce a basket of fruits and nuts add_storage_count(ITEM_REED_BASKETS, -1); add_storage_count(ITEM_BASKET_FOOD, 1); baskets_produced++; } // Apply basket breakage (from failed foraging attempts) if (baskets_broken > 0) { add_storage_count(ITEM_REED_BASKETS, -baskets_broken); if (x <= BASE_END) { string msg = tr("system.base.resident_basket_broke_foraging_one"); if (baskets_broken > 1) { dictionary basketForagingArgs; basketForagingArgs.set("count", baskets_broken); msg = trf("system.base.resident_basket_broke_foraging_many", basketForagingArgs); } speak_with_history(msg, true); } } // Notify of production if (baskets_produced > 0 && x <= BASE_END) { if (baskets_produced == 1) { speak_with_history(tr("system.base.resident_gathered_basket_food_one"), true); } else { dictionary basketFoodArgs; basketFoodArgs.set("count", baskets_produced); speak_with_history(trf("system.base.resident_gathered_basket_food_many", basketFoodArgs), true); } } }