Files
draugnorak/src/base_system.nvgt
2026-02-27 23:55:31 -05:00

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);
}
}
}