feat(calendar): add configurable calendar and stage ranges
Persist calendar units and inclusive life-stage duration ranges, expose them in the UI, and preserve defaults when loading legacy snapshots. Date formatting now derives from calendar settings. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
81581132ad
commit
ca371bd414
|
|
@ -4,6 +4,8 @@ A browser-based world simulation with a Rust server and a Leptos/Wasm UI. The se
|
||||||
|
|
||||||
Living simulation entities (people, rabbits, and wolves) run as Picard actors. The world coordinates terrain, shared resources, and actor messages; Rune behavior scripts remain on the simulation thread because Rune's VM is not `Send`.
|
Living simulation entities (people, rabbits, and wolves) run as Picard actors. The world coordinates terrain, shared resources, and actor messages; Rune behavior scripts remain on the simulation thread because Rune's VM is not `Send`.
|
||||||
|
|
||||||
|
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 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
|
||||||
|
|
|
||||||
235
src/world.rs
235
src/world.rs
|
|
@ -15,7 +15,112 @@ use std::time::Duration;
|
||||||
|
|
||||||
pub const W: usize = 96;
|
pub const W: usize = 96;
|
||||||
pub const H: usize = 64;
|
pub const H: usize = 64;
|
||||||
pub const DAY: u64 = 1800;
|
pub const DEFAULT_TICKS_PER_DAY: u64 = 1800;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
|
||||||
|
pub struct StageDuration {
|
||||||
|
pub min_days: u32,
|
||||||
|
pub max_days: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct CalendarSettings {
|
||||||
|
pub ticks_per_day: u64,
|
||||||
|
pub days_per_week: u32,
|
||||||
|
pub weeks_per_month: u32,
|
||||||
|
pub months_per_year: u32,
|
||||||
|
pub toddler: StageDuration,
|
||||||
|
pub child: StageDuration,
|
||||||
|
pub teen: StageDuration,
|
||||||
|
pub young_adult: StageDuration,
|
||||||
|
pub adult: StageDuration,
|
||||||
|
pub older_adult: StageDuration,
|
||||||
|
pub elderly: StageDuration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for StageDuration {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
min_days: 1,
|
||||||
|
max_days: 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CalendarSettings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
ticks_per_day: DEFAULT_TICKS_PER_DAY,
|
||||||
|
days_per_week: 7,
|
||||||
|
weeks_per_month: 4,
|
||||||
|
months_per_year: 12,
|
||||||
|
toddler: StageDuration {
|
||||||
|
min_days: 2,
|
||||||
|
max_days: 4,
|
||||||
|
},
|
||||||
|
child: StageDuration {
|
||||||
|
min_days: 4,
|
||||||
|
max_days: 8,
|
||||||
|
},
|
||||||
|
teen: StageDuration {
|
||||||
|
min_days: 4,
|
||||||
|
max_days: 8,
|
||||||
|
},
|
||||||
|
young_adult: StageDuration {
|
||||||
|
min_days: 6,
|
||||||
|
max_days: 12,
|
||||||
|
},
|
||||||
|
adult: StageDuration {
|
||||||
|
min_days: 20,
|
||||||
|
max_days: 40,
|
||||||
|
},
|
||||||
|
older_adult: StageDuration {
|
||||||
|
min_days: 10,
|
||||||
|
max_days: 20,
|
||||||
|
},
|
||||||
|
elderly: StageDuration {
|
||||||
|
min_days: 4,
|
||||||
|
max_days: 10,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CalendarSettings {
|
||||||
|
fn normalize(&mut self) {
|
||||||
|
self.ticks_per_day = self.ticks_per_day.clamp(1, 1_000_000);
|
||||||
|
self.days_per_week = self.days_per_week.clamp(1, 100);
|
||||||
|
self.weeks_per_month = self.weeks_per_month.clamp(1, 100);
|
||||||
|
self.months_per_year = self.months_per_year.clamp(1, 100);
|
||||||
|
for stage in [
|
||||||
|
"toddler",
|
||||||
|
"child",
|
||||||
|
"teen",
|
||||||
|
"young_adult",
|
||||||
|
"adult",
|
||||||
|
"older_adult",
|
||||||
|
"elderly",
|
||||||
|
] {
|
||||||
|
let range = self.duration_mut(stage).expect("known life stage");
|
||||||
|
range.min_days = range.min_days.clamp(1, 1_000_000);
|
||||||
|
range.max_days = range.max_days.clamp(range.min_days, 1_000_000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn duration_mut(&mut self, stage: &str) -> Option<&mut StageDuration> {
|
||||||
|
match stage {
|
||||||
|
"toddler" => Some(&mut self.toddler),
|
||||||
|
"child" => Some(&mut self.child),
|
||||||
|
"teen" => Some(&mut self.teen),
|
||||||
|
"young_adult" => Some(&mut self.young_adult),
|
||||||
|
"adult" => Some(&mut self.adult),
|
||||||
|
"older_adult" => Some(&mut self.older_adult),
|
||||||
|
"elderly" => Some(&mut self.elderly),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const WATER_DEEP: u8 = 0;
|
const WATER_DEEP: u8 = 0;
|
||||||
const WATER: u8 = 1;
|
const WATER: u8 = 1;
|
||||||
|
|
@ -258,6 +363,7 @@ pub struct World {
|
||||||
pub paused: bool,
|
pub paused: bool,
|
||||||
pub speed: u32,
|
pub speed: u32,
|
||||||
pub terrain_rev: u32,
|
pub terrain_rev: u32,
|
||||||
|
calendar: CalendarSettings,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
|
|
@ -283,6 +389,8 @@ struct WorldSnapshot {
|
||||||
paused: bool,
|
paused: bool,
|
||||||
speed: u32,
|
speed: u32,
|
||||||
terrain_rev: u32,
|
terrain_rev: u32,
|
||||||
|
#[serde(default)]
|
||||||
|
calendar: CalendarSettings,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cheb(a: (usize, usize), b: (usize, usize)) -> usize {
|
fn cheb(a: (usize, usize), b: (usize, usize)) -> usize {
|
||||||
|
|
@ -331,7 +439,7 @@ impl World {
|
||||||
stone: 0,
|
stone: 0,
|
||||||
tools: 0,
|
tools: 0,
|
||||||
rng: Rng(seed.wrapping_mul(0x9E3779B97F4A7C15) | 1),
|
rng: Rng(seed.wrapping_mul(0x9E3779B97F4A7C15) | 1),
|
||||||
tick: DAY / 4,
|
tick: DEFAULT_TICKS_PER_DAY / 4,
|
||||||
next_id: 1,
|
next_id: 1,
|
||||||
log: vec![],
|
log: vec![],
|
||||||
script: DEFAULT_SCRIPT.to_string(),
|
script: DEFAULT_SCRIPT.to_string(),
|
||||||
|
|
@ -339,6 +447,7 @@ impl World {
|
||||||
paused: false,
|
paused: false,
|
||||||
speed: 1,
|
speed: 1,
|
||||||
terrain_rev: 0,
|
terrain_rev: 0,
|
||||||
|
calendar: CalendarSettings::default(),
|
||||||
};
|
};
|
||||||
w.home = w
|
w.home = w
|
||||||
.find_land(GRASS)
|
.find_land(GRASS)
|
||||||
|
|
@ -522,15 +631,19 @@ impl World {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn say(&mut self, s: String) {
|
fn say(&mut self, s: String) {
|
||||||
self.log
|
self.log.push(format!(
|
||||||
.push(format!("[day {}] {}", self.tick / DAY + 1, s));
|
"[day {}] {}",
|
||||||
|
self.tick / self.calendar.ticks_per_day + 1,
|
||||||
|
s
|
||||||
|
));
|
||||||
if self.log.len() > 8 {
|
if self.log.len() > 8 {
|
||||||
self.log.remove(0);
|
self.log.remove(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn daylight(&self) -> f32 {
|
pub fn daylight(&self) -> f32 {
|
||||||
let phase = (self.tick % DAY) as f32 / DAY as f32;
|
let ticks_per_day = self.calendar.ticks_per_day;
|
||||||
|
let phase = (self.tick % ticks_per_day) as f32 / ticks_per_day as f32;
|
||||||
let v = -(phase * std::f32::consts::TAU).cos();
|
let v = -(phase * std::f32::consts::TAU).cos();
|
||||||
(0.5 + v * 0.8).clamp(0.0, 1.0)
|
(0.5 + v * 0.8).clamp(0.0, 1.0)
|
||||||
}
|
}
|
||||||
|
|
@ -906,6 +1019,9 @@ impl World {
|
||||||
let mut world = Self::new(seed);
|
let mut world = Self::new(seed);
|
||||||
world.speed = self.speed;
|
world.speed = self.speed;
|
||||||
world.paused = self.paused;
|
world.paused = self.paused;
|
||||||
|
world.calendar = self.calendar.clone();
|
||||||
|
world.calendar.normalize();
|
||||||
|
world.tick = world.calendar.ticks_per_day / 4;
|
||||||
if self.script != DEFAULT_SCRIPT {
|
if self.script != DEFAULT_SCRIPT {
|
||||||
world
|
world
|
||||||
.set_script(&self.script)
|
.set_script(&self.script)
|
||||||
|
|
@ -949,6 +1065,7 @@ impl World {
|
||||||
paused: self.paused,
|
paused: self.paused,
|
||||||
speed: self.speed,
|
speed: self.speed,
|
||||||
terrain_rev: self.terrain_rev,
|
terrain_rev: self.terrain_rev,
|
||||||
|
calendar: self.calendar.clone(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -994,7 +1111,9 @@ impl World {
|
||||||
paused: saved.paused,
|
paused: saved.paused,
|
||||||
speed: saved.speed,
|
speed: saved.speed,
|
||||||
terrain_rev: saved.terrain_rev,
|
terrain_rev: saved.terrain_rev,
|
||||||
|
calendar: saved.calendar,
|
||||||
};
|
};
|
||||||
|
world.calendar.normalize();
|
||||||
world.sync_entities();
|
world.sync_entities();
|
||||||
Ok(world)
|
Ok(world)
|
||||||
}
|
}
|
||||||
|
|
@ -1017,6 +1136,30 @@ impl World {
|
||||||
let num = |i: usize| t.get(i).and_then(|s| s.parse::<i64>().ok());
|
let num = |i: usize| t.get(i).and_then(|s| s.parse::<i64>().ok());
|
||||||
match t.first().copied() {
|
match t.first().copied() {
|
||||||
Some("pause") => self.paused = num(1) != Some(0),
|
Some("pause") => self.paused = num(1) != Some(0),
|
||||||
|
Some("calendar") => {
|
||||||
|
if let (Some(setting), Some(value)) = (t.get(1), num(2)) {
|
||||||
|
let value = value.clamp(1, 1_000_000) as u64;
|
||||||
|
match *setting {
|
||||||
|
"ticks_per_day" => self.calendar.ticks_per_day = value,
|
||||||
|
"days_per_week" => self.calendar.days_per_week = value.min(100) as u32,
|
||||||
|
"weeks_per_month" => self.calendar.weeks_per_month = value.min(100) as u32,
|
||||||
|
"months_per_year" => self.calendar.months_per_year = value.min(100) as u32,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some("stage") => {
|
||||||
|
if let (Some(stage), Some(min_days), Some(max_days)) = (t.get(1), num(2), num(3)) {
|
||||||
|
let min_days = min_days.clamp(1, 1_000_000) as u32;
|
||||||
|
let max_days = max_days.clamp(1, 1_000_000) as u32;
|
||||||
|
if min_days <= max_days {
|
||||||
|
if let Some(range) = self.calendar.duration_mut(stage) {
|
||||||
|
range.min_days = min_days;
|
||||||
|
range.max_days = max_days;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some("speed") => {
|
Some("speed") => {
|
||||||
if let Some(n) = num(1) {
|
if let Some(n) = num(1) {
|
||||||
self.speed = n.clamp(1, 8) as u32;
|
self.speed = n.clamp(1, 8) as u32;
|
||||||
|
|
@ -1200,16 +1343,30 @@ impl World {
|
||||||
|
|
||||||
pub fn state_json(&self) -> String {
|
pub fn state_json(&self) -> String {
|
||||||
let mut s = String::new();
|
let mut s = String::new();
|
||||||
|
let days_per_month =
|
||||||
|
self.calendar.days_per_week as u64 * self.calendar.weeks_per_month as u64;
|
||||||
|
let days_per_year = days_per_month * self.calendar.months_per_year as u64;
|
||||||
|
let day = self.tick / self.calendar.ticks_per_day;
|
||||||
|
let year = day / days_per_year + 1;
|
||||||
|
let month = (day / days_per_month) % self.calendar.months_per_year as u64 + 1;
|
||||||
|
let week =
|
||||||
|
(day / self.calendar.days_per_week as u64) % self.calendar.weeks_per_month as u64 + 1;
|
||||||
|
let date_day = day % self.calendar.days_per_week as u64 + 1;
|
||||||
let _ = write!(
|
let _ = write!(
|
||||||
s,
|
s,
|
||||||
"{{\"paused\":{},\"speed\":{},\"seed\":{},\"rev\":{},\"tick\":{},\"day\":{},\"hour\":{:.2},\"daylight\":{:.3},\"stock\":{{\"food\":{},\"wood\":{},\"stone\":{},\"tools\":{}}},\"home\":[{},{}],",
|
"{{\"paused\":{},\"speed\":{},\"seed\":{},\"rev\":{},\"tick\":{},\"day\":{},\"year\":{},\"month\":{},\"week\":{},\"date_day\":{},\"hour\":{:.2},\"daylight\":{:.3},\"stock\":{{\"food\":{},\"wood\":{},\"stone\":{},\"tools\":{}}},\"home\":[{},{}],",
|
||||||
self.paused,
|
self.paused,
|
||||||
self.speed,
|
self.speed,
|
||||||
self.seed,
|
self.seed,
|
||||||
self.terrain_rev,
|
self.terrain_rev,
|
||||||
self.tick,
|
self.tick,
|
||||||
self.tick / DAY + 1,
|
day + 1,
|
||||||
(self.tick % DAY) as f32 / DAY as f32 * 24.0,
|
year,
|
||||||
|
month,
|
||||||
|
week,
|
||||||
|
date_day,
|
||||||
|
(self.tick % self.calendar.ticks_per_day) as f32 / self.calendar.ticks_per_day as f32
|
||||||
|
* 24.0,
|
||||||
self.daylight(),
|
self.daylight(),
|
||||||
self.food,
|
self.food,
|
||||||
self.wood,
|
self.wood,
|
||||||
|
|
@ -1218,6 +1375,36 @@ impl World {
|
||||||
self.home.0,
|
self.home.0,
|
||||||
self.home.1
|
self.home.1
|
||||||
);
|
);
|
||||||
|
let _ = write!(
|
||||||
|
s,
|
||||||
|
"\"calendar\":{{\"ticks_per_day\":{},\"days_per_week\":{},\"weeks_per_month\":{},\"months_per_year\":{},",
|
||||||
|
self.calendar.ticks_per_day,
|
||||||
|
self.calendar.days_per_week,
|
||||||
|
self.calendar.weeks_per_month,
|
||||||
|
self.calendar.months_per_year
|
||||||
|
);
|
||||||
|
for (index, (name, range)) in [
|
||||||
|
("toddler", self.calendar.toddler),
|
||||||
|
("child", self.calendar.child),
|
||||||
|
("teen", self.calendar.teen),
|
||||||
|
("young_adult", self.calendar.young_adult),
|
||||||
|
("adult", self.calendar.adult),
|
||||||
|
("older_adult", self.calendar.older_adult),
|
||||||
|
("elderly", self.calendar.elderly),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
if index > 0 {
|
||||||
|
s.push(',');
|
||||||
|
}
|
||||||
|
let _ = write!(
|
||||||
|
s,
|
||||||
|
"\"{name}\":{{\"min_days\":{},\"max_days\":{}}}",
|
||||||
|
range.min_days, range.max_days
|
||||||
|
);
|
||||||
|
}
|
||||||
|
s.push_str("},");
|
||||||
s.push_str("\"houses\":[");
|
s.push_str("\"houses\":[");
|
||||||
let j: Vec<String> = self
|
let j: Vec<String> = self
|
||||||
.houses
|
.houses
|
||||||
|
|
@ -1278,7 +1465,7 @@ impl Drop for World {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{DAY, DEFAULT_SCRIPT, World};
|
use super::{DEFAULT_SCRIPT, DEFAULT_TICKS_PER_DAY, World};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn snapshot_restores_state_and_rng() {
|
fn snapshot_restores_state_and_rng() {
|
||||||
|
|
@ -1311,7 +1498,7 @@ mod tests {
|
||||||
assert_eq!(reset.speed, 4);
|
assert_eq!(reset.speed, 4);
|
||||||
assert!(reset.paused);
|
assert!(reset.paused);
|
||||||
assert_eq!(reset.people.len(), 9);
|
assert_eq!(reset.people.len(), 9);
|
||||||
assert_eq!(reset.tick, DAY / 4);
|
assert_eq!(reset.tick, DEFAULT_TICKS_PER_DAY / 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -1335,4 +1522,32 @@ mod tests {
|
||||||
world.people.len() + world.rabbits.len() + world.wolves.len()
|
world.people.len() + world.rabbits.len() + world.wolves.len()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn calendar_and_life_stage_ranges_persist_in_snapshots() {
|
||||||
|
let mut world = World::new(42);
|
||||||
|
world.apply("calendar ticks_per_day 240");
|
||||||
|
world.apply("calendar days_per_week 5");
|
||||||
|
world.apply("stage toddler 3 9");
|
||||||
|
|
||||||
|
let snapshot = world.snapshot_json().unwrap();
|
||||||
|
let restored = World::from_snapshot(&snapshot, 42).unwrap();
|
||||||
|
assert_eq!(restored.calendar.ticks_per_day, 240);
|
||||||
|
assert_eq!(restored.calendar.days_per_week, 5);
|
||||||
|
assert_eq!(restored.calendar.toddler.min_days, 3);
|
||||||
|
assert_eq!(restored.calendar.toddler.max_days, 9);
|
||||||
|
assert_eq!(world.state_json(), restored.state_json());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn snapshots_without_calendar_use_default_settings() {
|
||||||
|
let world = World::new(42);
|
||||||
|
let mut snapshot: serde_json::Value =
|
||||||
|
serde_json::from_str(&world.snapshot_json().unwrap()).unwrap();
|
||||||
|
snapshot.as_object_mut().unwrap().remove("calendar");
|
||||||
|
|
||||||
|
let restored = World::from_snapshot(&snapshot.to_string(), 42).unwrap();
|
||||||
|
assert_eq!(restored.calendar.ticks_per_day, DEFAULT_TICKS_PER_DAY);
|
||||||
|
assert_eq!(restored.calendar.days_per_week, 7);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -151,7 +151,10 @@ fn App() -> impl IntoView {
|
||||||
{move || state.with(|s| s.as_ref().map(|s| {
|
{move || state.with(|s| s.as_ref().map(|s| {
|
||||||
let (h, m) = (s.hour.floor() as u32, ((s.hour % 1.0) * 60.0) as u32);
|
let (h, m) = (s.hour.floor() as u32, ((s.hour % 1.0) * 60.0) as u32);
|
||||||
view! {
|
view! {
|
||||||
<div>{format!("Day {}, {h:02}:{m:02}", s.day)}</div>
|
<div>{format!(
|
||||||
|
"Year {}, month {}, week {}, day {}, {h:02}:{m:02}",
|
||||||
|
s.year, s.month, s.week, s.date_day
|
||||||
|
)}</div>
|
||||||
<div>{format!("People: {}", s.people.len())}</div>
|
<div>{format!("People: {}", s.people.len())}</div>
|
||||||
<div>{format!("Seed: {}", s.seed)}</div>
|
<div>{format!("Seed: {}", s.seed)}</div>
|
||||||
<div>{format!("Rabbits: {}", s.rabbits.len())}</div>
|
<div>{format!("Rabbits: {}", s.rabbits.len())}</div>
|
||||||
|
|
@ -193,6 +196,37 @@ fn App() -> impl IntoView {
|
||||||
} title="Generate a new world with a different seed">"Regenerate"</button>
|
} title="Generate a new world with a different seed">"Regenerate"</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>"Calendar and life-stage durations"</summary>
|
||||||
|
<p>"Stage durations are random inclusive ranges in in-game days."</p>
|
||||||
|
<SettingInput state=state label="Ticks per day" setting="calendar.ticks_per_day".to_string() />
|
||||||
|
<SettingInput state=state label="Days per week" setting="calendar.days_per_week".to_string() />
|
||||||
|
<SettingInput state=state label="Weeks per month" setting="calendar.weeks_per_month".to_string() />
|
||||||
|
<SettingInput state=state label="Months per year" setting="calendar.months_per_year".to_string() />
|
||||||
|
{["toddler", "child", "teen", "young_adult", "adult", "older_adult", "elderly"]
|
||||||
|
.into_iter()
|
||||||
|
.map(|stage| {
|
||||||
|
let min_setting = format!("stage.{stage}.min_days");
|
||||||
|
let max_setting = format!("stage.{stage}.max_days");
|
||||||
|
view! {
|
||||||
|
<div class="stage-settings">
|
||||||
|
<strong>{stage.replace('_', " ")}</strong>
|
||||||
|
<SettingInput
|
||||||
|
state=state
|
||||||
|
label="min days"
|
||||||
|
setting=min_setting
|
||||||
|
/>
|
||||||
|
<SettingInput
|
||||||
|
state=state
|
||||||
|
label="max days"
|
||||||
|
setting=max_setting
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect_view()}
|
||||||
|
</details>
|
||||||
|
|
||||||
<h3>"Tool"</h3>
|
<h3>"Tool"</h3>
|
||||||
<div>
|
<div>
|
||||||
<select on:change=move |e| {
|
<select on:change=move |e| {
|
||||||
|
|
@ -265,6 +299,44 @@ fn App() -> impl IntoView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn SettingInput(
|
||||||
|
state: RwSignal<Option<State>>,
|
||||||
|
label: &'static str,
|
||||||
|
setting: String,
|
||||||
|
) -> impl IntoView {
|
||||||
|
let input_setting = setting.clone();
|
||||||
|
view! {
|
||||||
|
<label class="setting">
|
||||||
|
{label}
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
max="1000000"
|
||||||
|
prop:value=move || {
|
||||||
|
state.with(|s| {
|
||||||
|
s.as_ref()
|
||||||
|
.and_then(|s| s.calendar.setting(&setting))
|
||||||
|
.unwrap_or(1)
|
||||||
|
.to_string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
on:change=move |event| {
|
||||||
|
if let Ok(value) = event_target_value(&event).parse::<u64>() {
|
||||||
|
let command = state.with_untracked(|s| {
|
||||||
|
s.as_ref()
|
||||||
|
.and_then(|s| s.calendar.command(&input_setting, value))
|
||||||
|
});
|
||||||
|
if let Some(command) = command {
|
||||||
|
api::edit(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
fn PersonEditor(p: Person) -> impl IntoView {
|
fn PersonEditor(p: Person) -> impl IntoView {
|
||||||
let id = p.0;
|
let id = p.0;
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,80 @@ pub struct Stock {
|
||||||
pub tools: u32,
|
pub tools: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone)]
|
||||||
|
pub struct StageRange {
|
||||||
|
pub min_days: u32,
|
||||||
|
pub max_days: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone)]
|
||||||
|
pub struct Calendar {
|
||||||
|
pub ticks_per_day: u64,
|
||||||
|
pub days_per_week: u32,
|
||||||
|
pub weeks_per_month: u32,
|
||||||
|
pub months_per_year: u32,
|
||||||
|
pub toddler: StageRange,
|
||||||
|
pub child: StageRange,
|
||||||
|
pub teen: StageRange,
|
||||||
|
pub young_adult: StageRange,
|
||||||
|
pub adult: StageRange,
|
||||||
|
pub older_adult: StageRange,
|
||||||
|
pub elderly: StageRange,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Calendar {
|
||||||
|
pub fn setting(&self, name: &str) -> Option<u64> {
|
||||||
|
match name {
|
||||||
|
"ticks_per_day" => Some(self.ticks_per_day),
|
||||||
|
"days_per_week" => Some(self.days_per_week.into()),
|
||||||
|
"weeks_per_month" => Some(self.weeks_per_month.into()),
|
||||||
|
"months_per_year" => Some(self.months_per_year.into()),
|
||||||
|
_ => {
|
||||||
|
let (stage, bound) = name.split_once('.')?;
|
||||||
|
let range = match stage {
|
||||||
|
"toddler" => &self.toddler,
|
||||||
|
"child" => &self.child,
|
||||||
|
"teen" => &self.teen,
|
||||||
|
"young_adult" => &self.young_adult,
|
||||||
|
"adult" => &self.adult,
|
||||||
|
"older_adult" => &self.older_adult,
|
||||||
|
"elderly" => &self.elderly,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
match bound {
|
||||||
|
"min_days" => Some(range.min_days.into()),
|
||||||
|
"max_days" => Some(range.max_days.into()),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn command(&self, name: &str, value: u64) -> Option<String> {
|
||||||
|
if let Some(setting) = name.strip_prefix("calendar.") {
|
||||||
|
return Some(format!("calendar {setting} {value}"));
|
||||||
|
}
|
||||||
|
let rest = name.strip_prefix("stage.")?;
|
||||||
|
let (stage, bound) = rest.split_once('.')?;
|
||||||
|
let range = match stage {
|
||||||
|
"toddler" => &self.toddler,
|
||||||
|
"child" => &self.child,
|
||||||
|
"teen" => &self.teen,
|
||||||
|
"young_adult" => &self.young_adult,
|
||||||
|
"adult" => &self.adult,
|
||||||
|
"older_adult" => &self.older_adult,
|
||||||
|
"elderly" => &self.elderly,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
let (min_days, max_days) = match bound {
|
||||||
|
"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),
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
Some(format!("stage {stage} {min_days} {max_days}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// id, x, y, action, hunger, food, wood, stone
|
/// id, x, y, action, hunger, food, wood, stone
|
||||||
#[derive(Deserialize, Clone)]
|
#[derive(Deserialize, Clone)]
|
||||||
pub struct Person(
|
pub struct Person(
|
||||||
|
|
@ -48,6 +122,10 @@ pub struct State {
|
||||||
pub seed: u64,
|
pub seed: u64,
|
||||||
pub rev: u32,
|
pub rev: u32,
|
||||||
pub day: u64,
|
pub day: u64,
|
||||||
|
pub year: u64,
|
||||||
|
pub month: u64,
|
||||||
|
pub week: u64,
|
||||||
|
pub date_day: u64,
|
||||||
pub hour: f32,
|
pub hour: f32,
|
||||||
pub daylight: f32,
|
pub daylight: f32,
|
||||||
pub stock: Stock,
|
pub stock: Stock,
|
||||||
|
|
@ -58,6 +136,7 @@ pub struct State {
|
||||||
pub wolves: Vec<Animal>,
|
pub wolves: Vec<Animal>,
|
||||||
pub res: Vec<Res>,
|
pub res: Vec<Res>,
|
||||||
pub log: Vec<String>,
|
pub log: Vec<String>,
|
||||||
|
pub calendar: Calendar,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Clone)]
|
#[derive(Deserialize, Clone)]
|
||||||
|
|
|
||||||
18
ui/style.css
18
ui/style.css
|
|
@ -48,6 +48,24 @@ canvas {
|
||||||
width: 56px;
|
width: 56px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stage-settings {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-settings strong {
|
||||||
|
width: 92px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
margin: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
.log div {
|
.log div {
|
||||||
margin: 2px 0;
|
margin: 2px 0;
|
||||||
color: #aaa;
|
color: #aaa;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue