Move stored items up to higher priority stockpiles

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

author
Marijn Besseling <njirambem@gmail.com> · 2026-10-24 22:38 UTC
commit
37868d82453e0716af3982889226b7c17f3f4a9a
parent
4ee3e12f13
tree
browse at this commit

4 files changed +78 -21

CLAUDE.md +1 -1

@@ -44,7 +44,7 @@ DF/RimWorld-style colony sim, Rust + Bevy 0.20 (newer than training data — che
44 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.
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): drops homes of extinct families; roofs finished homes (`roofs_needed`: interior enclosed exactly); plans one home per partnered family without one via `HomeContext::plan` (BFS from colony home, center ≥ HOME_MIN_DISTANCE, core open + 1-tile belt free of stockpile/farm/blueprints/homes) → 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. 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 -- 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 on tiles that don't accept them as loose (no re-hauling between priorities). `ColonyGoals` overrides (`adjust` ±, `reset`, `is_overridden`) win over learned goals.
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.
48 48 - `src/panel.rs` top-right colonist panel (also: stockpile section with accept toggles + priority cycle when a stockpile tile is selected; collapsible Stock targets with -/+/auto per Stock): roster of `ColonistButton`s (rebuilt when names/child/mood face change; click selects) and, for a selected adult, `WorkToggle` buttons per `WorkType::{Food, Build, Gather, Haul, Tame}` toggling the colonist's `WorkPriorities` (required on Colonist; red = disabled). choose_jobs' autonomous candidates are a `(WorkType, finder)` list filtered by `allows`; orders ignore priorities.
49 49 - `src/rng.rs` `SimRng` SplitMix64 resource, seeded (deterministic sim; no `rand` dep).
50 50

src/jobs/mod.rs +44 -18

@@ -456,6 +456,32 @@ pub fn find_work(
456 456 found
457 457 }
458 458
459 +fn stacks_by_tile<'a>(
460 + items: impl IntoIterator<Item = (&'a Item, &'a TilePos)>,
461 +) -> HashMap<UVec2, (ItemKind, u32)> {
462 + let mut stacks: HashMap<UVec2, (ItemKind, u32)> = HashMap::new();
463 + for (item, pos) in items {
464 + stacks.entry(pos.0).or_insert((item.kind, 0)).1 += item.count;
465 + }
466 + stacks
467 +}
468 +
469 +fn storage_candidates<'z>(
470 + zones: &'z Zones,
471 + reservations: &'z Reservations,
472 + stacks: &'z HashMap<UVec2, (ItemKind, u32)>,
473 + kind: ItemKind,
474 +) -> impl Iterator<Item = UVec2> + 'z {
475 + zones.stockpile_tiles().filter(move |&t| {
476 + zones.accepts(t, kind)
477 + && reservations.is_free(Target::Storage(t))
478 + && match stacks.get(&t) {
479 + None => true,
480 + Some(&(other, count)) => other == kind && count < STACK_LIMIT,
481 + }
482 + })
483 +}
484 +
459 485 pub fn find_storage<'a>(
460 486 map: &WorldMap,
461 487 zones: &Zones,
@@ -464,26 +490,12 @@ pub fn find_storage<'a>(
464 490 from: UVec2,
465 491 kind: ItemKind,
466 492 ) -> Option<UVec2> {
467 - let mut stacks: HashMap<UVec2, (ItemKind, u32)> = HashMap::new();
468 - for (item, pos) in items {
469 - let entry = stacks.entry(pos.0).or_insert((item.kind, 0));
470 - entry.1 += item.count;
471 - }
472 - let room = |tile: UVec2| match stacks.get(&tile) {
473 - None => true,
474 - Some(&(other, count)) => other == kind && count < STACK_LIMIT,
475 - };
493 + let stacks = stacks_by_tile(items);
476 494 StoragePriority::DESCENDING
477 495 .into_iter()
478 496 .find_map(|priority| {
479 - let candidates: HashSet<UVec2> = zones
480 - .stockpile_tiles()
481 - .filter(|&t| {
482 - zones.rule(t).priority == priority
483 - && zones.accepts(t, kind)
484 - && reservations.is_free(Target::Storage(t))
485 - && room(t)
486 - })
497 + let candidates: HashSet<UVec2> = storage_candidates(zones, reservations, &stacks, kind)
498 + .filter(|&t| zones.rule(t).priority == priority)
487 499 .collect();
488 500 nearest_of(map, from, SEARCH_TILES, &candidates)
489 501 })
@@ -496,10 +508,24 @@ pub fn find_haul(
496 508 items: &[(Item, TilePos)],
497 509 from: UVec2,
498 510 ) -> Option<Task> {
511 + let stacks = stacks_by_tile(items.iter().map(|(i, p)| (i, p)));
512 + let mut best: HashMap<ItemKind, StoragePriority> = HashMap::new();
513 + let misplaced = |item: &Item, tile: UVec2, best: &mut HashMap<ItemKind, StoragePriority>| {
514 + if !zones.accepts(tile, item.kind) {
515 + return true;
516 + }
517 + let top = *best.entry(item.kind).or_insert_with(|| {
518 + storage_candidates(zones, reservations, &stacks, item.kind)
519 + .map(|t| zones.rule(t).priority)
520 + .max()
521 + .unwrap_or(StoragePriority::Low)
522 + });
523 + zones.rule(tile).priority < top
524 + };
499 525 let loose: HashSet<UVec2> = items
500 526 .iter()
501 527 .filter(|(item, pos)| {
502 - !zones.accepts(pos.0, item.kind) && reservations.is_free(Target::Pickup(pos.0))
528 + reservations.is_free(Target::Pickup(pos.0)) && misplaced(item, pos.0, &mut best)
503 529 })
504 530 .map(|(_, pos)| pos.0)
505 531 .collect();

src/jobs/tests.rs +32 -0

@@ -1356,3 +1356,35 @@ fn hauled_items_go_to_the_highest_priority_stockpile_that_accepts_them() {
1356 1356 let in_high = |t: UVec2| t.x >= 15 && t.y >= 15;
1357 1357 assert!(placed.iter().all(|(_, t)| in_high(*t)), "{placed:?}");
1358 1358 }
1359 +
1360 +#[test]
1361 +fn stored_items_move_up_to_a_higher_priority_stockpile() {
1362 + let mut app = test_app(map_with_trees(&[]));
1363 + set_goals(&mut app, &[]);
1364 + put_beds(&mut app, 1);
1365 + let map = app.world().resource::<WorldMap>().clone();
1366 + {
1367 + let mut zones = app.world_mut().resource_mut::<Zones>();
1368 + zones.add_stockpile(&map, IVec2::new(3, 3), IVec2::new(4, 4));
1369 + zones.add_stockpile(&map, IVec2::new(14, 14), IVec2::new(15, 15));
1370 + }
1371 + put_items(&mut app, UVec2::new(3, 3), ItemKind::Wood, 5);
1372 + spawn_colonist(&mut app, UVec2::new(5, 5));
1373 + run_ticks(&mut app, 60 * 5);
1374 + let wood_at = |app: &mut App| {
1375 + let world = app.world_mut();
1376 + world
1377 + .query::<(&Item, &TilePos)>()
1378 + .iter(world)
1379 + .map(|(_, p)| p.0)
1380 + .collect::<Vec<_>>()
1381 + };
1382 + assert_eq!(wood_at(&mut app), vec![UVec2::new(3, 3)]);
1383 + app.world_mut()
1384 + .resource_mut::<Zones>()
1385 + .update_region_rule(UVec2::new(14, 14), |rule| {
1386 + rule.priority = crate::zones::StoragePriority::High
1387 + });
1388 + run_ticks(&mut app, 60 * 20);
1389 + assert!(wood_at(&mut app).iter().all(|t| t.x >= 14 && t.y >= 14));
1390 +}

tasks.md +1 -2

@@ -33,10 +33,9 @@
33 33 - [x] Animals flee, ranged hunting, carcasses butchered into meat, taming with breeding herds and slaughter of spare tame animals
34 34 - [x] Mood (needs, meals, partner, home, sickness, grief), food poisoning from raw food, unhappy colonists leave, ageing and death of old age
35 35 - [x] Colonist panel: roster with mood faces (click to select) and per-colonist work type toggles
36 -- [x] Stockpile filters and priority per connected stockpile; stock target overrides in the panel
36 +- [x] Stockpile filters and priority per connected stockpile (stock moves up to higher priorities); stock target overrides in the panel
37 37 - [x] Smooth hill shading overlay
38 38 - [x] Action/keybind system, toolbar buttons for every keybind, hide-UI toggle (H), clicks on UI don't reach the map
39 39
40 40 ## Planned
41 -- [ ] Re-haul items from low to high priority stockpiles
42 41 - [ ] Sim performance: per-tile scans in `BuildContext::buildable` and site searches; wasm ticks still ~1 ms each with a full colony