feat(ui): expose life stages and fertility settings

Show each person's life stage, age, and partner in the inspector. Add per-stage fertility toggles and document the life cycle and birth conditions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Steve Biedermann 2026-10-11 17:48:35 +02:00
parent 2cb4d4e626
commit a0f18df9ef
3 changed files with 45 additions and 5 deletions

View File

@ -6,6 +6,8 @@ Living simulation entities (people, rabbits, and wolves) run as Picard actors. T
The **Calendar and life-stage durations** panel configures ticks per day and the number of days, weeks, and months in larger calendar units. Each human life stage has an inclusive minimum/maximum duration in in-game days; the world samples within that range when a person enters the stage. These settings are stored with the world snapshot. The **Calendar and life-stage durations** panel configures ticks per day and the number of days, weeks, and months in larger calendar units. Each human life stage has an inclusive minimum/maximum duration in in-game days; the world samples within that range when a person enters the stage. These settings are stored with the world snapshot.
New people, including babies, start as toddlers and age through child, teen, young adult, adult, older adult, elderly, and death. The starting village is seeded with young and mature adults so the simulation can work and form the first partnerships immediately. Unpartnered people from young adulthood onward can form a single partnership with a nearby eligible person. Each partnership has up to one birth attempt per calendar month, with a 20% chance of having a toddler when both partners are in stages marked fertile, the stockpile has enough food, and housing capacity permits it. Young adult and adult stages are fertile by default; fertility can be changed per stage in the calendar panel. Partnerships are Picard actors as well as people and animals.
The UI is built automatically as part of `cargo run` and embedded in the server binary. There is no separate manual UI build step. The UI is built automatically as part of `cargo run` and embedded in the server binary. There is no separate manual UI build step.
## Getting Started ## Getting Started

View File

@ -221,6 +221,7 @@ fn App() -> impl IntoView {
label="max days" label="max days"
setting=max_setting setting=max_setting
/> />
<StageFertility state=state stage=stage.to_string() />
</div> </div>
} }
}) })
@ -337,6 +338,34 @@ fn SettingInput(
} }
} }
#[component]
fn StageFertility(state: RwSignal<Option<State>>, stage: String) -> impl IntoView {
let stage_for_change = stage.clone();
view! {
<label class="setting">
<input
type="checkbox"
prop:checked=move || {
let setting = format!("stage.{stage}.fertile");
state.with(|s| {
s.as_ref()
.and_then(|s| s.calendar.setting(&setting))
.unwrap_or(0)
!= 0
})
}
on:change=move |event| {
api::edit(format!(
"stage {stage_for_change} fertile {}",
u8::from(event_target_checked(&event))
));
}
/>
" fertile"
</label>
}
}
#[component] #[component]
fn PersonEditor(p: Person) -> impl IntoView { fn PersonEditor(p: Person) -> impl IntoView {
let id = p.0; let id = p.0;

View File

@ -24,6 +24,7 @@ pub struct Stock {
pub struct StageRange { pub struct StageRange {
pub min_days: u32, pub min_days: u32,
pub max_days: u32, pub max_days: u32,
pub fertile: bool,
} }
#[derive(Deserialize, Clone)] #[derive(Deserialize, Clone)]
@ -43,13 +44,14 @@ pub struct Calendar {
impl Calendar { impl Calendar {
pub fn setting(&self, name: &str) -> Option<u64> { pub fn setting(&self, name: &str) -> Option<u64> {
match name { match name.strip_prefix("calendar.").unwrap_or(name) {
"ticks_per_day" => Some(self.ticks_per_day), "ticks_per_day" => Some(self.ticks_per_day),
"days_per_week" => Some(self.days_per_week.into()), "days_per_week" => Some(self.days_per_week.into()),
"weeks_per_month" => Some(self.weeks_per_month.into()), "weeks_per_month" => Some(self.weeks_per_month.into()),
"months_per_year" => Some(self.months_per_year.into()), "months_per_year" => Some(self.months_per_year.into()),
_ => { _ => {
let (stage, bound) = name.split_once('.')?; let rest = name.strip_prefix("stage.").unwrap_or(name);
let (stage, bound) = rest.split_once('.')?;
let range = match stage { let range = match stage {
"toddler" => &self.toddler, "toddler" => &self.toddler,
"child" => &self.child, "child" => &self.child,
@ -63,6 +65,7 @@ impl Calendar {
match bound { match bound {
"min_days" => Some(range.min_days.into()), "min_days" => Some(range.min_days.into()),
"max_days" => Some(range.max_days.into()), "max_days" => Some(range.max_days.into()),
"fertile" => Some(range.fertile.into()),
_ => None, _ => None,
} }
} }
@ -85,6 +88,9 @@ impl Calendar {
"elderly" => &self.elderly, "elderly" => &self.elderly,
_ => return None, _ => return None,
}; };
if bound == "fertile" {
return Some(format!("stage {stage} fertile {}", u8::from(value != 0)));
}
let (min_days, max_days) = match bound { let (min_days, max_days) = match bound {
"min_days" => (value.min(u32::MAX as u64) as u32, range.max_days), "min_days" => (value.min(u32::MAX as u64) as u32, range.max_days),
"max_days" => (range.min_days, value.min(u32::MAX as u64) as u32), "max_days" => (range.min_days, value.min(u32::MAX as u64) as u32),
@ -94,7 +100,7 @@ impl Calendar {
} }
} }
/// id, x, y, action, hunger, food, wood, stone /// id, x, y, action, hunger, food, wood, stone, life stage, age in days, partner id
#[derive(Deserialize, Clone)] #[derive(Deserialize, Clone)]
pub struct Person( pub struct Person(
pub u32, pub u32,
@ -105,6 +111,9 @@ pub struct Person(
pub u32, pub u32,
pub u32, pub u32,
pub u32, pub u32,
pub String,
pub u64,
pub Option<u32>,
); );
/// x, y, energy /// x, y, energy
@ -289,8 +298,8 @@ pub fn describe(sel: Sel, s: &State, t: &Terrain) -> Vec<String> {
)); ));
for p in &people { for p in &people {
out.push(format!( out.push(format!(
"- Person #{}: {}, hunger {}, carrying food {} wood {} stone {}", "- Person #{}: {}, stage {}, age {} days, hunger {}, partner {:?}, carrying food {} wood {} stone {}",
p.0, p.3, p.4, p.5, p.6, p.7 p.0, p.3, p.8, p.9, p.4, p.10, p.5, p.6, p.7
)); ));
} }
if rabbits.len() + wolves.len() <= 6 { if rabbits.len() + wolves.len() <= 6 {