1075 lines
36 KiB
Plaintext
1075 lines
36 KiB
Plaintext
// 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);
|
|
}
|
|
}
|
|
}
|