Raise colonist cap to 20 and speed up job searches
- author
- Marijn Besseling <njirambem@gmail.com> · 2026-10-25 08:48 UTC
- commit
- 681918401f9559346c54b8dd5bfe53fb56b0b54a
- parent
- 2f2f76394a
- tree
- browse at this commit
8 files changed +140 -46
| CLAUDE.md | +3 -1 | |
| src/colonist.rs | +3 -2 | |
| src/family.rs | +10 -25 | |
| src/items.rs | +4 -0 | |
| src/jobs/mod.rs | +73 -17 | |
| src/pathfinding.rs | +45 -0 | |
| src/population.rs | +1 -1 | |
| tasks.md | +1 -0 |
CLAUDE.md +3 -1
| @@ -41,7 +41,7 @@ DF/RimWorld-style colony sim, Rust + Bevy 0.20 (newer than training data — che | ||
| 41 | 41 | - `src/farming.rs` `plan_farm` (every 600 ticks, once a stockpile exists and no farm): `FarmContext::plan` picks the FARM_SIZE² square near home (BFS corners) maximising soil − distance where every tile is farmable, plant-free and ≥ FARM_CLEARANCE from stockpile, blueprints, structures and items. |
| 42 | 42 | - `src/paths.rs` traffic wear. `follow_path` adds 1 wear when a colonist (not an animal) enters a tile. `update_paths` (hourly): wear × WEAR_KEPT_PER_HOUR (0.98); Grass/Soil/Sand with wear ≥ PATH_WEAR (8), no plant, not farm → `Terrain::Path`; Path with wear < REGROW_WEAR → Grass. `plan_paving` (hourly) keeps up to PAVE_BATCH `BuildKind::Pave` blueprints on the busiest Path tiles with wear ≥ PAVE_WEAR (20); built with 1 stone → `Terrain::Paved`. Path cost 80, Paved 60 (= CHEAPEST_MOVE_COST, which A*'s heuristic uses; a test enforces no terrain is cheaper), so colonists prefer and reinforce them. Hover shows wear. Tuned from observed traffic of 3 colonists: busiest tiles reach ~15–30 wear by day 3–4. |
| 43 | 43 | - `src/economy.rs` learned stock targets. `Stock::{Wood, Stone, Iron, Gold, RawFood, Meals}` groups item kinds (raw foods are one group). `ColonyGoals` keeps `use_per_day` per stock (EMA, ~24 h memory; seeded with guesses), `record_use(kind, n)` is called by jobs for real consumption (eating, cooking input, build material — not spoilage), `learn_needs` (hourly) updates rates and goals = clamp(rate × buffer days (2 materials, 1.5 food), min, 300) + material for pending blueprints (`planned_needs`). `wanted(kind, stocks, on_the_way)` compares the whole group. Stock counts only ground items `in_colony` (Chebyshev ≤ `COLONY_RADIUS` 60 of home; colonist searches cover it, tested) plus carried ones — far stray stacks used to satisfy goals while unreachable (jobs, info panel and arrivals all use this). `ColonyGoals::fixed` for tests. |
| 44 | -- `src/population.rs` `ColonyLog` (last 4 events, shown in the info panel). `starve`: `Starvation` ticks at zero hunger; after STARVATION_TICKS (1 day) the colonist dies (drops load, releases reservations). `arrivals` daily at 10:00: if `prosperous` (≤ MAX_COLONISTS, stored nutrition ≥ 1 per colonist, a free bed or enough wood for one) and a 60% roll, a newcomer (`spawn_colonist`, next unused name) appears at an `edge_tile` with an unordered MoveTo home. | |
| 44 | +- `src/population.rs` `ColonyLog` (last 4 events, shown in the info panel). `starve`: `Starvation` ticks at zero hunger; after STARVATION_TICKS (1 day) the colonist dies (drops load, releases reservations). `arrivals` daily at 10:00: if `prosperous` (< MAX_COLONISTS 20, stored nutrition ≥ 1 per colonist, a free bed or enough wood for one) and a 60% roll, a newcomer (`spawn_colonist`, next unused name) appears at an `edge_tile` with an unordered MoveTo home. | |
| 45 | 45 | - `src/family.rs` families. `Family(u32)` (shared by partners and their children), `Partner(Entity)`, `Child{born}` (CHILD_SCALE visual, grows up after CHILDHOOD_TICKS = 4 days in `grow_up`) on colonists. `bond` (hourly): unpartnered adults not of the same family within BOND_RANGE accumulate `Affinity` hours per pair; ≥ BOND_HOURS + BOND_CHANCE roll → partners with a new family id; dangling Partner removed when the partner dies. `Homes{by_family: family → Home{lo, hi}}` (wall-ring corners, interior HOME_INTERIOR 4×3, `bed_tiles` 2 beds top-left, `owner(tile)`). `plan_homes` (every 300 ticks, Decide): homes of extinct families move to `Homes.vacant` (serde default; `all()`, `is_home()` cover owned + vacant; `owner()` owned only, so vacant beds count as communal for sleeping); `abandoned_home` adopts enclosed HOME_INTERIOR rooms around unowned beds as vacant (old saves); couples without a home take the vacant home nearest the colony before a new one is planned. Floods `reachable` from colony home (if walkable) and drops blueprints with no reachable tile in their 3×3 (sealed pockets inflated goals forever). Per home: `door_repair` (no door/planned door whose `outward` tile is reachable → Door on an empty door site, else Deconstruct the best wall; runs even with blueprints pending), else roofs (`roofs_needed`), statue for owned built homes. `surplus_beds`: reachable communal beds beyond homeless adults → Deconstruct (nearest kept). New homes via `HomeContext::plan` (BFS from colony home, center ≥ HOME_MIN_DISTANCE, core open + 1-tile belt unclaimed AND walkable — homes used to be packed wall to wall with doors facing a neighbour's wall) → wall/door/bed blueprints (door nearest colony home). `births` (daily 20:00): family with 2 living partners, built home (roofed, 2 beds), < MAX_CHILDREN, colony < MAX_COLONISTS and food ≥ colonists, BIRTH_CHANCE → child spawned inside home. Children: choose_jobs gives them only sleep/eat/wander; `find_bed_site` counts adults. Sleep: own home bed (adults), free roofed home floor (children), else communal beds (`Homes::owner` None) → campfire. `plan_shelter` ignores beds inside homes (owned or vacant). Selection text shows partner / child days left. |
| 46 | 46 | - `src/mood.rs` `Mood(f32)` (default 0.6) eases hourly (1/MOOD_HOURS) toward `mood_target(MoodFactors)`: hunger, rest, last meal cooked/raw within a day, `Sick`, partner, built family home, grief (a `Losses` tick within 2 days; starvation and old age push losses). `Ate{colonist, kind}` message from run_jobs' Eat completion → `digest`: `LastMeal`, raw food rolls `poison_chance` (meat 15%, berries/veg 4%) → `Sick{until}` 12 h (work rate halved in run_jobs). `leave`: adults below LEAVE_MOOD for LEAVE_AFTER_HOURS (48, `Unhappy` hours) leave (despawn, drop load). `Age{born}` (1 day = 1 year; `assign_age` gives new adults 18–40, children 0); `old_age` daily: from OLD_AGE 60, death chance 3%/year over. Selection shows mood/age/sick. |
| 47 | 47 | - Storage rules (zones.rs): `Zones.rules` per stockpile tile `StorageRule{refused: Vec<Stock>, priority: StoragePriority::{Low, Normal, High}}` (absent = default), `accepts(tile, kind)`, `update_region_rule` applies to the 4-connected stockpile region; removing tiles drops rules. `find_storage` tries priorities High→Normal→Low, nearest accepting free tile with room; `find_haul` treats items as loose when their tile doesn't accept them or a higher-priority accepting tile with room exists (`storage_candidates`), so stock moves up the priorities and settles. `ColonyGoals` overrides (`adjust` ±, `reset`, `is_overridden`) win over learned goals. |
| @@ -56,6 +56,8 @@ DF/RimWorld-style colony sim, Rust + Bevy 0.20 (newer than training data — che | ||
| 56 | 56 | - Hot spots fixed so far: predicate BFS scanning items per tile (use `nearest_of` with precollected sets), searches that cannot match (pre-check wanted kinds, empty farm sets), choose_jobs setup skipped unless someone is ready (idle timer ticked in a pre-pass), periodic passes over only `WorldMap::worn_tiles` / `growing_plants`, light-driven tilemap re-tints rate-limited to 100 ms, `BuildContext.occupied` set. |
| 57 | 57 | - Searches and A* share `can_step` (no corner cutting): a target the BFS could reach diagonally but A* couldn't made A* exhaust the map (~10 ms) many times a second on a 50-day save. Mined yields drop on the miner's tile when the site is only diagonally reachable. |
| 58 | 58 | - Profiling a saved colony: copy the browser save to `simulator-save.json` (gitignored; the native build reads it with F9), temporarily add a Startup system writing `Action::Load` + `SetSpeed(Max)`. `sample` sometimes captures only a handful of samples on the first try — rerun. |
| 59 | +- choose_jobs: wanted kinds computed once per colonist into an array (`ItemKind::index`), `occupied` set built once per pass; `find_blueprint_work` pre-collects stand tiles of affordable unreserved blueprints (none → no search); `find_haul` only counts loose stacks whose kind has a storage candidate and uses `nearest_of`; gather `find_work` failures are memoised (`GatherMemo`: same wanted-kind mask → skip for GATHER_RETRY_TICKS). `Reach::flood` (bitmap) for plan_homes' reachability. | |
| 60 | +- Browser at Max on the 20-colonist day-220 save: ~8 game days per 30 s (was 3.5–6 before the pass above). | |
| 59 | 61 | - Browser at Max: ~100–350× on a 12-colonist colony, dips at dusk (ambient steps re-tint all chunks) and on hourly ticks. |
| 60 | 62 | |
| 61 | 63 | ## Gotchas |
src/colonist.rs +3 -2
| @@ -27,9 +27,10 @@ impl Plugin for ColonistPlugin { | ||
| 27 | 27 | |
| 28 | 28 | const TILES_PER_SECOND: f32 = 4.0; |
| 29 | 29 | |
| 30 | -pub const NAMES: [&str; 16] = [ | |
| 30 | +pub const NAMES: [&str; 26] = [ | |
| 31 | 31 | "Ada", "Bram", "Cleo", "Dana", "Emil", "Fenna", "Gijs", "Hanna", "Ivo", "Juul", "Kees", |
| 32 | - "Lotte", "Milan", "Noor", "Otto", "Pien", | |
| 32 | + "Lotte", "Milan", "Noor", "Otto", "Pien", "Quinn", "Roos", "Sem", "Tess", "Ugo", "Vera", | |
| 33 | + "Wout", "Xem", "Yara", "Zeno", | |
| 33 | 34 | ]; |
| 34 | 35 | |
| 35 | 36 | const COLORS: [Color; 8] = [ |
src/family.rs +10 -25
| @@ -8,7 +8,7 @@ use crate::{ | ||
| 8 | 8 | colonist::{Colonist, ColonistAssets, NAMES, spawn_colonist}, |
| 9 | 9 | economy::in_colony, |
| 10 | 10 | items::{Item, ItemAssets}, |
| 11 | - pathfinding::nearest_tile, | |
| 11 | + pathfinding::{Reach, nearest_tile}, | |
| 12 | 12 | population::{ColonyLog, MAX_COLONISTS, food_in_store}, |
| 13 | 13 | rng::SimRng, |
| 14 | 14 | sim::{SimSystems, SimTick, TICKS_PER_HOUR}, |
| @@ -356,13 +356,13 @@ impl HomeContext<'_> { | ||
| 356 | 356 | pub fn door_repair( |
| 357 | 357 | map: &WorldMap, |
| 358 | 358 | doors: &HashSet<UVec2>, |
| 359 | - reachable: &HashSet<UVec2>, | |
| 359 | + reachable: &Reach, | |
| 360 | 360 | home: &Home, |
| 361 | 361 | toward: UVec2, |
| 362 | 362 | ) -> Option<(UVec2, BuildKind)> { |
| 363 | 363 | let open_outside = |t: &UVec2| { |
| 364 | 364 | let out = home.outward(*t); |
| 365 | - out.min_element() >= 0 && reachable.contains(&out.as_uvec2()) | |
| 365 | + out.min_element() >= 0 && reachable.contains(out.as_uvec2()) | |
| 366 | 366 | }; |
| 367 | 367 | if home |
| 368 | 368 | .door_sites() |
| @@ -484,21 +484,11 @@ fn plan_homes( | ||
| 484 | 484 | let home = homes.vacant.swap_remove(index); |
| 485 | 485 | homes.by_family.insert(family, home); |
| 486 | 486 | } |
| 487 | - let mut reachable = HashSet::new(); | |
| 488 | - nearest_tile(&map, colony_home, usize::MAX, |p| { | |
| 489 | - reachable.insert(p); | |
| 490 | - false | |
| 491 | - }); | |
| 487 | + let reachable = Reach::flood(&map, colony_home); | |
| 492 | 488 | let anchored = map.move_cost(colony_home.as_ivec2()).is_some(); |
| 493 | - blueprints.0.retain(|tile, _| { | |
| 494 | - !anchored | |
| 495 | - || (-1..=1).any(|dy| { | |
| 496 | - (-1..=1).any(|dx| { | |
| 497 | - let near = tile.as_ivec2() + IVec2::new(dx, dy); | |
| 498 | - near.min_element() >= 0 && reachable.contains(&near.as_uvec2()) | |
| 499 | - }) | |
| 500 | - }) | |
| 501 | - }); | |
| 489 | + blueprints | |
| 490 | + .0 | |
| 491 | + .retain(|tile, _| !anchored || reachable.near(*tile)); | |
| 502 | 492 | let owned: HashSet<Home> = homes.by_family.values().copied().collect(); |
| 503 | 493 | let door = BuildKind::Structure(StructureKind::Door); |
| 504 | 494 | let entries: HashSet<UVec2> = blueprints |
| @@ -532,7 +522,7 @@ fn plan_homes( | ||
| 532 | 522 | .count(); |
| 533 | 523 | let communal: Vec<UVec2> = beds |
| 534 | 524 | .iter() |
| 535 | - .filter(|b| !homes.is_home(**b) && blueprints.get(**b).is_none() && reachable.contains(b)) | |
| 525 | + .filter(|b| !homes.is_home(**b) && blueprints.get(**b).is_none() && reachable.contains(**b)) | |
| 536 | 526 | .copied() |
| 537 | 527 | .collect(); |
| 538 | 528 | for bed in surplus_beds(&communal, homeless_adults, colony_home) { |
| @@ -844,13 +834,8 @@ mod tests { | ||
| 844 | 834 | HashSet::from([door]) |
| 845 | 835 | } |
| 846 | 836 | |
| 847 | - fn reachable(map: &WorldMap, from: UVec2) -> HashSet<UVec2> { | |
| 848 | - let mut seen = HashSet::new(); | |
| 849 | - nearest_tile(map, from, usize::MAX, |p| { | |
| 850 | - seen.insert(p); | |
| 851 | - false | |
| 852 | - }); | |
| 853 | - seen | |
| 837 | + fn reachable(map: &WorldMap, from: UVec2) -> Reach { | |
| 838 | + Reach::flood(map, from) | |
| 854 | 839 | } |
| 855 | 840 | |
| 856 | 841 | // New homes keep a walkable tile all around, so any door they get opens onto open ground. |
src/items.rs +4 -0
| @@ -48,6 +48,10 @@ impl ItemKind { | ||
| 48 | 48 | ItemKind::Tools, |
| 49 | 49 | ]; |
| 50 | 50 | |
| 51 | + pub fn index(self) -> usize { | |
| 52 | + self as usize | |
| 53 | + } | |
| 54 | + | |
| 51 | 55 | pub fn nutrition(self) -> f32 { |
| 52 | 56 | match self { |
| 53 | 57 | ItemKind::Berries | ItemKind::Meat | ItemKind::Vegetables => 0.1, |
src/jobs/mod.rs +73 -17
| @@ -498,6 +498,14 @@ fn reserve_task(reservations: &mut Reservations, task: Task, by: Entity) { | ||
| 498 | 498 | } |
| 499 | 499 | } |
| 500 | 500 | |
| 501 | +const GATHER_RETRY_TICKS: u64 = 300; | |
| 502 | + | |
| 503 | +#[derive(Default)] | |
| 504 | +pub struct GatherMemo { | |
| 505 | + mask: u32, | |
| 506 | + until: u64, | |
| 507 | +} | |
| 508 | + | |
| 501 | 509 | pub fn find_work( |
| 502 | 510 | map: &WorldMap, |
| 503 | 511 | reservations: &Reservations, |
| @@ -602,17 +610,24 @@ pub fn find_haul( | ||
| 602 | 610 | }); |
| 603 | 611 | zones.rule(tile).priority < top |
| 604 | 612 | }; |
| 613 | + let mut room: HashMap<ItemKind, bool> = HashMap::new(); | |
| 605 | 614 | let loose: HashSet<UVec2> = items |
| 606 | 615 | .iter() |
| 607 | 616 | .filter(|(item, pos)| { |
| 608 | - reservations.is_free(Target::Pickup(pos.0)) && misplaced(item, pos.0, &mut best) | |
| 617 | + reservations.is_free(Target::Pickup(pos.0)) | |
| 618 | + && misplaced(item, pos.0, &mut best) | |
| 619 | + && *room.entry(item.kind).or_insert_with(|| { | |
| 620 | + storage_candidates(zones, reservations, &stacks, item.kind) | |
| 621 | + .next() | |
| 622 | + .is_some() | |
| 623 | + }) | |
| 609 | 624 | }) |
| 610 | 625 | .map(|(_, pos)| pos.0) |
| 611 | 626 | .collect(); |
| 612 | 627 | if loose.is_empty() { |
| 613 | 628 | return None; |
| 614 | 629 | } |
| 615 | - let pickup = nearest_tile(map, from, SEARCH_TILES, |p| loose.contains(&p))?; | |
| 630 | + let pickup = nearest_of(map, from, SEARCH_TILES, &loose)?; | |
| 616 | 631 | let (item, _) = items.iter().find(|(_, pos)| pos.0 == pickup)?; |
| 617 | 632 | let refs = items.iter().map(|(i, p)| (i, p)); |
| 618 | 633 | let to = find_storage(map, zones, reservations, refs, pickup, item.kind)?; |
| @@ -1053,11 +1068,36 @@ pub fn find_bed_site(ctx: &BuildContext, colonists: usize, from: UVec2) -> Optio | ||
| 1053 | 1068 | } |
| 1054 | 1069 | |
| 1055 | 1070 | pub fn find_blueprint_work(ctx: &BuildContext, from: UVec2) -> Option<Task> { |
| 1056 | - if ctx.blueprints.0.is_empty() { | |
| 1071 | + let mut stock: HashMap<ItemKind, u32> = HashMap::new(); | |
| 1072 | + for (item, pos) in ctx.items { | |
| 1073 | + if ctx.reservations.is_free(Target::Pickup(pos.0)) { | |
| 1074 | + *stock.entry(item.kind).or_insert(0) += item.count; | |
| 1075 | + } | |
| 1076 | + } | |
| 1077 | + let mut stands: HashSet<UVec2> = HashSet::new(); | |
| 1078 | + for (&site, &kind) in &ctx.blueprints.0 { | |
| 1079 | + let (material, amount) = kind.cost(); | |
| 1080 | + let affordable = amount == 0 || stock.get(&material).copied().unwrap_or(0) >= amount; | |
| 1081 | + if !affordable || !ctx.reservations.is_free(Target::Site(site)) { | |
| 1082 | + continue; | |
| 1083 | + } | |
| 1084 | + for d in std::iter::once(IVec2::ZERO).chain(NEIGHBORS) { | |
| 1085 | + let stand = site.as_ivec2() + d; | |
| 1086 | + if (d == IVec2::ZERO || ctx.map.move_cost(site.as_ivec2()).is_none()) | |
| 1087 | + && ctx.map.move_cost(stand).is_some() | |
| 1088 | + { | |
| 1089 | + stands.insert(stand.as_uvec2()); | |
| 1090 | + } | |
| 1091 | + } | |
| 1092 | + } | |
| 1093 | + if stands.is_empty() { | |
| 1057 | 1094 | return None; |
| 1058 | 1095 | } |
| 1059 | 1096 | let mut found = None; |
| 1060 | 1097 | nearest_tile(ctx.map, from, SEARCH_TILES, |p| { |
| 1098 | + if !stands.contains(&p) { | |
| 1099 | + return false; | |
| 1100 | + } | |
| 1061 | 1101 | found = std::iter::once(IVec2::ZERO) |
| 1062 | 1102 | .chain(NEIGHBORS) |
| 1063 | 1103 | .filter_map(|d| { |
| @@ -1123,6 +1163,7 @@ fn choose_jobs( | ||
| 1123 | 1163 | With<Colonist>, |
| 1124 | 1164 | >, |
| 1125 | 1165 | plans: (Res<Homes>, Res<Designations>), |
| 1166 | + mut gather_memo: Local<GatherMemo>, | |
| 1126 | 1167 | ) { |
| 1127 | 1168 | let (homes, designations) = plans; |
| 1128 | 1169 | let mut anyone_ready = false; |
| @@ -1188,6 +1229,11 @@ fn choose_jobs( | ||
| 1188 | 1229 | .map(|(_, _, pos)| pos.0) |
| 1189 | 1230 | .collect(); |
| 1190 | 1231 | let hour = hour_of_day(*tick); |
| 1232 | + let mut occupied: HashSet<UVec2> = structures_list | |
| 1233 | + .iter() | |
| 1234 | + .map(|(pos, ..)| *pos) | |
| 1235 | + .chain(items.iter().map(|(_, _, pos)| pos.0)) | |
| 1236 | + .collect(); | |
| 1191 | 1237 | for ( |
| 1192 | 1238 | entity, |
| 1193 | 1239 | tile, |
| @@ -1223,7 +1269,8 @@ fn choose_jobs( | ||
| 1223 | 1269 | let snapshot: Vec<(Item, TilePos)> = |
| 1224 | 1270 | items.iter().map(|(_, item, pos)| (*item, *pos)).collect(); |
| 1225 | 1271 | let refs = snapshot.iter().map(|(i, p)| (i, p)); |
| 1226 | - let wanted = |kind: ItemKind| goals.wanted(kind, &stocks, &pending); | |
| 1272 | + let wanted_kinds = ItemKind::ALL.map(|kind| goals.wanted(kind, &stocks, &pending)); | |
| 1273 | + let wanted = |kind: ItemKind| wanted_kinds[kind.index()]; | |
| 1227 | 1274 | let sleep = SleepContext { |
| 1228 | 1275 | map: &map, |
| 1229 | 1276 | zones: &zones, |
| @@ -1279,12 +1326,7 @@ fn choose_jobs( | ||
| 1279 | 1326 | { |
| 1280 | 1327 | Some(Task::Equip { pickup }) |
| 1281 | 1328 | } else { |
| 1282 | - let occupied: HashSet<UVec2> = structures_list | |
| 1283 | - .iter() | |
| 1284 | - .map(|(pos, ..)| *pos) | |
| 1285 | - .chain(planned.iter().map(|(pos, _)| *pos)) | |
| 1286 | - .chain(snapshot.iter().map(|(_, pos)| pos.0)) | |
| 1287 | - .collect(); | |
| 1329 | + occupied.extend(planned.iter().map(|(pos, _)| *pos)); | |
| 1288 | 1330 | let build = BuildContext { |
| 1289 | 1331 | map: &map, |
| 1290 | 1332 | zones: &zones, |
| @@ -1320,6 +1362,12 @@ fn choose_jobs( | ||
| 1320 | 1362 | }) |
| 1321 | 1363 | .flatten() |
| 1322 | 1364 | }; |
| 1365 | + let gather_mask = ItemKind::ALL | |
| 1366 | + .iter() | |
| 1367 | + .filter(|&&k| wanted(k)) | |
| 1368 | + .fold(0u32, |mask, k| mask | 1 << k.index()); | |
| 1369 | + let skip_gather = gather_memo.mask == gather_mask && tick.0 < gather_memo.until; | |
| 1370 | + let gather_failed = std::cell::Cell::new(false); | |
| 1323 | 1371 | let candidates: [(WorkType, &dyn Fn() -> Option<Task>); 16] = [ |
| 1324 | 1372 | (WorkType::Haul, &|| { |
| 1325 | 1373 | find_expansion(&map, &zones, &reservations, &snapshot, &blueprints) |
| @@ -1354,11 +1402,12 @@ fn choose_jobs( | ||
| 1354 | 1402 | (WorkType::Craft, &toolmaking), |
| 1355 | 1403 | (WorkType::Build, &|| find_blueprint_work(&build, tile.0)), |
| 1356 | 1404 | (WorkType::Gather, &|| { |
| 1357 | - ItemKind::ALL | |
| 1358 | - .iter() | |
| 1359 | - .any(|&k| wanted(k)) | |
| 1360 | - .then(|| find_work(&map, &reservations, tile.0, |_, k| wanted(k))) | |
| 1361 | - .flatten() | |
| 1405 | + if gather_mask == 0 || skip_gather { | |
| 1406 | + return None; | |
| 1407 | + } | |
| 1408 | + let work = find_work(&map, &reservations, tile.0, |_, k| wanted(k)); | |
| 1409 | + gather_failed.set(work.is_none()); | |
| 1410 | + work | |
| 1362 | 1411 | }), |
| 1363 | 1412 | (WorkType::Food, &|| { |
| 1364 | 1413 | wanted(ItemKind::Meat) |
| @@ -1379,10 +1428,17 @@ fn choose_jobs( | ||
| 1379 | 1428 | find_haul(&map, &zones, &reservations, &snapshot, tile.0) |
| 1380 | 1429 | }), |
| 1381 | 1430 | ]; |
| 1382 | - candidates | |
| 1431 | + let task = candidates | |
| 1383 | 1432 | .iter() |
| 1384 | 1433 | .filter(|(work, _)| priorities.allows(*work)) |
| 1385 | - .find_map(|(_, find)| find()) | |
| 1434 | + .find_map(|(_, find)| find()); | |
| 1435 | + if gather_failed.get() { | |
| 1436 | + *gather_memo = GatherMemo { | |
| 1437 | + mask: gather_mask, | |
| 1438 | + until: tick.0 + GATHER_RETRY_TICKS, | |
| 1439 | + }; | |
| 1440 | + } | |
| 1441 | + task | |
| 1386 | 1442 | }; |
| 1387 | 1443 | if let Some(task) = task { |
| 1388 | 1444 | if let Task::Work { site, .. } = task |
src/pathfinding.rs +45 -0
| @@ -83,6 +83,51 @@ pub fn nearest_tile( | ||
| 83 | 83 | None |
| 84 | 84 | } |
| 85 | 85 | |
| 86 | +pub struct Reach { | |
| 87 | + width: u32, | |
| 88 | + seen: Vec<bool>, | |
| 89 | +} | |
| 90 | + | |
| 91 | +impl Reach { | |
| 92 | + pub fn flood(map: &WorldMap, start: UVec2) -> Self { | |
| 93 | + let width = map.size.x; | |
| 94 | + let mut seen = vec![false; map.size.element_product() as usize]; | |
| 95 | + let index = |p: UVec2| (p.y * width + p.x) as usize; | |
| 96 | + if map.move_cost(start.as_ivec2()).is_some() { | |
| 97 | + seen[index(start)] = true; | |
| 98 | + let mut queue = VecDeque::from([start]); | |
| 99 | + while let Some(pos) = queue.pop_front() { | |
| 100 | + for step in NEIGHBORS { | |
| 101 | + let next = pos.as_ivec2() + step; | |
| 102 | + if can_step(map, pos.as_ivec2(), step) && !seen[index(next.as_uvec2())] { | |
| 103 | + seen[index(next.as_uvec2())] = true; | |
| 104 | + queue.push_back(next.as_uvec2()); | |
| 105 | + } | |
| 106 | + } | |
| 107 | + } | |
| 108 | + } | |
| 109 | + Self { width, seen } | |
| 110 | + } | |
| 111 | + | |
| 112 | + pub fn contains(&self, p: UVec2) -> bool { | |
| 113 | + p.x < self.width | |
| 114 | + && self | |
| 115 | + .seen | |
| 116 | + .get((p.y * self.width + p.x) as usize) | |
| 117 | + .copied() | |
| 118 | + .unwrap_or(false) | |
| 119 | + } | |
| 120 | + | |
| 121 | + pub fn near(&self, p: UVec2) -> bool { | |
| 122 | + (-1..=1).any(|dy| { | |
| 123 | + (-1..=1).any(|dx| { | |
| 124 | + let q = p.as_ivec2() + IVec2::new(dx, dy); | |
| 125 | + q.min_element() >= 0 && self.contains(q.as_uvec2()) | |
| 126 | + }) | |
| 127 | + }) | |
| 128 | + } | |
| 129 | +} | |
| 130 | + | |
| 86 | 131 | pub fn search_reach(max_tiles: usize) -> u32 { |
| 87 | 132 | (max_tiles as f32).sqrt().ceil() as u32 |
| 88 | 133 | } |
src/population.rs +1 -1
| @@ -32,7 +32,7 @@ impl Plugin for PopulationPlugin { | ||
| 32 | 32 | } |
| 33 | 33 | |
| 34 | 34 | pub const STARVATION_TICKS: u32 = 24 * TICKS_PER_HOUR as u32; |
| 35 | -pub const MAX_COLONISTS: usize = 12; | |
| 35 | +pub const MAX_COLONISTS: usize = 20; | |
| 36 | 36 | pub const ARRIVAL_CHANCE: f32 = 0.6; |
| 37 | 37 | const ARRIVAL_HOUR_TICK: u64 = 4 * TICKS_PER_HOUR; |
| 38 | 38 | const DAY_TICKS: u64 = 24 * TICKS_PER_HOUR; |
tasks.md +1 -0
| @@ -42,6 +42,7 @@ | ||
| 42 | 42 | - [x] Save/load (F5/F9, toolbar buttons): browser localStorage on web, simulator-save.json natively |
| 43 | 43 | - [x] Max speed fix: target searches respect A*'s no-corner-cutting rule (unreachable diagonal stacks caused repeated full-map path searches) |
| 44 | 44 | - [x] Housing upkeep: vacant homes kept and reused by new couples, abandoned enclosed homes adopted, blocked doors repaired, homes keep a walkable gap, unreachable blueprints dropped, surplus communal beds deconstructed |
| 45 | +- [x] Colonist cap raised to 20 (26 names); job search pass to keep Max speed up at 20 colonists | |
| 45 | 46 | - [x] Action/keybind system, toolbar buttons for every keybind, hide-UI toggle (H), clicks on UI don't reach the map |
| 46 | 47 | |
| 47 | 48 | ## Planned |