refactor(ui): resolve clippy pedantic lints
Replace lossy casts with checked tile/canvas helpers, use map_or_else and infallible conversions, and switch the build script to assert!. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
5a1384cc21
commit
9fd93a8534
5
build.rs
5
build.rs
|
|
@ -35,11 +35,10 @@ fn main() {
|
||||||
let status = cargo
|
let status = cargo
|
||||||
.status()
|
.status()
|
||||||
.expect("failed to run cargo for the ui crate");
|
.expect("failed to run cargo for the ui crate");
|
||||||
if !status.success() {
|
assert!(
|
||||||
panic!(
|
status.success(),
|
||||||
"building ui/ failed (see the cargo output above); the wasm32-unknown-unknown target must be installed: rustup target add wasm32-unknown-unknown"
|
"building ui/ failed (see the cargo output above); the wasm32-unknown-unknown target must be installed: rustup target add wasm32-unknown-unknown"
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
let profile = if release { "release" } else { "debug" };
|
let profile = if release { "release" } else { "debug" };
|
||||||
let wasm = target_dir
|
let wasm = target_dir
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ pub fn connect(on_state: impl Fn(String) + 'static) {
|
||||||
"ws"
|
"ws"
|
||||||
};
|
};
|
||||||
let Ok(host) = loc.host() else { return };
|
let Ok(host) = loc.host() else { return };
|
||||||
open(format!("{proto}://{host}/ws"), on_state.clone());
|
open(&format!("{proto}://{host}/ws"), on_state.clone());
|
||||||
|
|
||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
loop {
|
loop {
|
||||||
|
|
@ -45,8 +45,8 @@ pub fn connect(on_state: impl Fn(String) + 'static) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open(url: String, on_state: Rc<dyn Fn(String)>) {
|
fn open(url: &str, on_state: Rc<dyn Fn(String)>) {
|
||||||
let Ok(ws) = WebSocket::new(&url) else { return };
|
let Ok(ws) = WebSocket::new(url) else { return };
|
||||||
let cb = on_state.clone();
|
let cb = on_state.clone();
|
||||||
let onmessage = Closure::<dyn FnMut(MessageEvent)>::new(move |e: MessageEvent| {
|
let onmessage = Closure::<dyn FnMut(MessageEvent)>::new(move |e: MessageEvent| {
|
||||||
if let Some(text) = e.data().as_string() {
|
if let Some(text) = e.data().as_string() {
|
||||||
|
|
@ -56,13 +56,13 @@ fn open(url: String, on_state: Rc<dyn Fn(String)>) {
|
||||||
ws.set_onmessage(Some(onmessage.as_ref().unchecked_ref()));
|
ws.set_onmessage(Some(onmessage.as_ref().unchecked_ref()));
|
||||||
onmessage.forget();
|
onmessage.forget();
|
||||||
|
|
||||||
let url2 = url.clone();
|
let url = url.to_string();
|
||||||
let onclose = Closure::<dyn FnMut()>::new(move || {
|
let onclose = Closure::<dyn FnMut()>::new(move || {
|
||||||
SOCKET.with(|s| *s.borrow_mut() = None);
|
SOCKET.with(|s| *s.borrow_mut() = None);
|
||||||
let (url, on_state) = (url2.clone(), on_state.clone());
|
let (url, on_state) = (url.clone(), on_state.clone());
|
||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
TimeoutFuture::new(1000).await;
|
TimeoutFuture::new(1000).await;
|
||||||
open(url, on_state);
|
open(&url, on_state);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
ws.set_onclose(Some(onclose.as_ref().unchecked_ref()));
|
ws.set_onclose(Some(onclose.as_ref().unchecked_ref()));
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::model::{Sel, State, T, Terrain};
|
use crate::model::{Sel, State, T, Terrain, canvas_len, tile_offset};
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement};
|
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement};
|
||||||
|
|
||||||
|
|
@ -7,8 +7,8 @@ const COLORS: [&str; 7] = [
|
||||||
];
|
];
|
||||||
|
|
||||||
pub fn draw(canvas: &HtmlCanvasElement, t: &Terrain, s: &State, sel: Option<Sel>) {
|
pub fn draw(canvas: &HtmlCanvasElement, t: &Terrain, s: &State, sel: Option<Sel>) {
|
||||||
canvas.set_width((t.w as f64 * T) as u32);
|
canvas.set_width(canvas_len(t.w));
|
||||||
canvas.set_height((t.h as f64 * T) as u32);
|
canvas.set_height(canvas_len(t.h));
|
||||||
let Some(ctx) = canvas
|
let Some(ctx) = canvas
|
||||||
.get_context("2d")
|
.get_context("2d")
|
||||||
.ok()
|
.ok()
|
||||||
|
|
@ -21,7 +21,7 @@ pub fn draw(canvas: &HtmlCanvasElement, t: &Terrain, s: &State, sel: Option<Sel>
|
||||||
for y in 0..t.h {
|
for y in 0..t.h {
|
||||||
for x in 0..t.w {
|
for x in 0..t.w {
|
||||||
ctx.set_fill_style_str(COLORS[t.kind(x, y)]);
|
ctx.set_fill_style_str(COLORS[t.kind(x, y)]);
|
||||||
ctx.fill_rect(x as f64 * T, y as f64 * T, T, T);
|
ctx.fill_rect(tile_offset(x), tile_offset(y), T, T);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -29,8 +29,8 @@ pub fn draw(canvas: &HtmlCanvasElement, t: &Terrain, s: &State, sel: Option<Sel>
|
||||||
ctx.set_fill_style_str(color);
|
ctx.set_fill_style_str(color);
|
||||||
ctx.begin_path();
|
ctx.begin_path();
|
||||||
let _ = ctx.arc(
|
let _ = ctx.arc(
|
||||||
x as f64 * T + T / 2.0,
|
tile_offset(x) + T / 2.0,
|
||||||
y as f64 * T + T / 2.0,
|
tile_offset(y) + T / 2.0,
|
||||||
r,
|
r,
|
||||||
0.0,
|
0.0,
|
||||||
std::f64::consts::TAU,
|
std::f64::consts::TAU,
|
||||||
|
|
@ -44,16 +44,21 @@ pub fn draw(canvas: &HtmlCanvasElement, t: &Terrain, s: &State, sel: Option<Sel>
|
||||||
2 => dot(r.0, r.1, 2.0, "#e85a8a"),
|
2 => dot(r.0, r.1, 2.0, "#e85a8a"),
|
||||||
_ => {
|
_ => {
|
||||||
ctx.set_fill_style_str("#666");
|
ctx.set_fill_style_str("#666");
|
||||||
ctx.fill_rect(r.0 as f64 * T + 2.0, r.1 as f64 * T + 2.0, 6.0, 6.0);
|
ctx.fill_rect(tile_offset(r.0) + 2.0, tile_offset(r.1) + 2.0, 6.0, 6.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctx.set_fill_style_str("#7a4a22");
|
ctx.set_fill_style_str("#7a4a22");
|
||||||
for h in &s.houses {
|
for h in &s.houses {
|
||||||
ctx.fill_rect(h.0 as f64 * T + 1.0, h.1 as f64 * T + 1.0, T - 2.0, T - 2.0);
|
ctx.fill_rect(
|
||||||
|
tile_offset(h.0) + 1.0,
|
||||||
|
tile_offset(h.1) + 1.0,
|
||||||
|
T - 2.0,
|
||||||
|
T - 2.0,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
ctx.set_fill_style_str("#b05a2a");
|
ctx.set_fill_style_str("#b05a2a");
|
||||||
ctx.fill_rect(s.home.0 as f64 * T, s.home.1 as f64 * T, T, T);
|
ctx.fill_rect(tile_offset(s.home.0), tile_offset(s.home.1), T, T);
|
||||||
for a in &s.rabbits {
|
for a in &s.rabbits {
|
||||||
dot(a.0, a.1, 2.5, "#fff");
|
dot(a.0, a.1, 2.5, "#fff");
|
||||||
}
|
}
|
||||||
|
|
@ -65,17 +70,17 @@ pub fn draw(canvas: &HtmlCanvasElement, t: &Terrain, s: &State, sel: Option<Sel>
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.set_fill_style_str(&format!("rgba(5,10,50,{})", (1.0 - s.daylight) * 0.65));
|
ctx.set_fill_style_str(&format!("rgba(5,10,50,{})", (1.0 - s.daylight) * 0.65));
|
||||||
ctx.fill_rect(0.0, 0.0, t.w as f64 * T, t.h as f64 * T);
|
ctx.fill_rect(0.0, 0.0, tile_offset(t.w), tile_offset(t.h));
|
||||||
|
|
||||||
if let Some(sel) = sel {
|
if let Some(sel) = sel {
|
||||||
let (x0, y0, x1, y1) = sel.rect();
|
let (x0, y0, x1, y1) = sel.rect();
|
||||||
ctx.set_stroke_style_str("#fff");
|
ctx.set_stroke_style_str("#fff");
|
||||||
ctx.set_line_width(2.0);
|
ctx.set_line_width(2.0);
|
||||||
ctx.stroke_rect(
|
ctx.stroke_rect(
|
||||||
x0 as f64 * T,
|
tile_offset(x0),
|
||||||
y0 as f64 * T,
|
tile_offset(y0),
|
||||||
(x1 - x0 + 1) as f64 * T,
|
tile_offset(x1 - x0 + 1),
|
||||||
(y1 - y0 + 1) as f64 * T,
|
tile_offset(y1 - y0 + 1),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ mod model;
|
||||||
use leptos::mount::mount_to_body;
|
use leptos::mount::mount_to_body;
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos::task::spawn_local;
|
use leptos::task::spawn_local;
|
||||||
use model::{Person, Sel, State, T, Terrain};
|
use model::{Person, Sel, State, Terrain, tile_at};
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
#[wasm_bindgen(start)]
|
#[wasm_bindgen(start)]
|
||||||
|
|
@ -53,7 +53,7 @@ fn App() -> impl IntoView {
|
||||||
if let (Some(c), Some(s), Some(t)) = (canvas.get(), s.as_ref(), t.as_ref()) {
|
if let (Some(c), Some(s), Some(t)) = (canvas.get(), s.as_ref(), t.as_ref()) {
|
||||||
canvas::draw(&c, t, s, sel);
|
canvas::draw(&c, t, s, sel);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -62,17 +62,14 @@ fn App() -> impl IntoView {
|
||||||
// Map through the displayed size so CSS scaling or zoom can't skew the tile.
|
// Map through the displayed size so CSS scaling or zoom can't skew the tile.
|
||||||
let r = c.get_bounding_client_rect();
|
let r = c.get_bounding_client_rect();
|
||||||
let (sx, sy) = (
|
let (sx, sy) = (
|
||||||
c.width() as f64 / r.width().max(1.0),
|
f64::from(c.width()) / r.width().max(1.0),
|
||||||
c.height() as f64 / r.height().max(1.0),
|
f64::from(c.height()) / r.height().max(1.0),
|
||||||
);
|
);
|
||||||
let px = (ev.client_x() as f64 - r.left()) * sx;
|
let px = (f64::from(ev.client_x()) - r.left()) * sx;
|
||||||
let py = (ev.client_y() as f64 - r.top()) * sy;
|
let py = (f64::from(ev.client_y()) - r.top()) * sy;
|
||||||
terrain.with_untracked(|t| {
|
terrain.with_untracked(|t| {
|
||||||
t.as_ref().map(|t| {
|
t.as_ref()
|
||||||
let x = ((px / T).max(0.0) as usize).min(t.w - 1);
|
.map(|t| (tile_at(px, t.w - 1), tile_at(py, t.h - 1)))
|
||||||
let y = ((py / T).max(0.0) as usize).min(t.h - 1);
|
|
||||||
(x, y)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -149,10 +146,10 @@ fn App() -> impl IntoView {
|
||||||
<div class="side">
|
<div class="side">
|
||||||
<h3>"World"</h3>
|
<h3>"World"</h3>
|
||||||
{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(), (s.hour.fract() * 60.0).floor());
|
||||||
view! {
|
view! {
|
||||||
<div>{format!(
|
<div>{format!(
|
||||||
"Year {}, month {}, week {}, day {}, {h:02}:{m:02}",
|
"Year {}, month {}, week {}, day {}, {h:02.0}:{m:02.0}",
|
||||||
s.year, s.month, s.week, s.date_day
|
s.year, s.month, s.week, s.date_day
|
||||||
)}</div>
|
)}</div>
|
||||||
<div>{format!("People: {}", s.people.len())}</div>
|
<div>{format!("People: {}", s.people.len())}</div>
|
||||||
|
|
@ -181,7 +178,7 @@ fn App() -> impl IntoView {
|
||||||
}))}
|
}))}
|
||||||
<div>
|
<div>
|
||||||
<button on:click=move |_| {
|
<button on:click=move |_| {
|
||||||
api::edit(format!("pause {}", if paused() { 0 } else { 1 }))
|
api::edit(format!("pause {}", u8::from(!paused())));
|
||||||
}>{move || if paused() { "Resume" } else { "Pause" }}</button>
|
}>{move || if paused() { "Resume" } else { "Pause" }}</button>
|
||||||
" Speed "
|
" Speed "
|
||||||
<select
|
<select
|
||||||
|
|
@ -278,13 +275,14 @@ fn App() -> impl IntoView {
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
prop:max=move || state.with(|s| s.as_ref().map(|s| {
|
prop:max=move || state.with(|s| s.as_ref().map_or_else(
|
||||||
match tool.get().as_str() {
|
|| "100".to_string(),
|
||||||
|
|s| match tool.get().as_str() {
|
||||||
"energy" => s.limits.animal_energy.to_string(),
|
"energy" => s.limits.animal_energy.to_string(),
|
||||||
"spawn" => s.limits.spawn.to_string(),
|
"spawn" => s.limits.spawn.to_string(),
|
||||||
_ => s.limits.resource.to_string(),
|
_ => s.limits.resource.to_string(),
|
||||||
}
|
},
|
||||||
}).unwrap_or_else(|| "100".to_string()))
|
))
|
||||||
title="amount / energy"
|
title="amount / energy"
|
||||||
prop:value=move || amount.get()
|
prop:value=move || amount.get()
|
||||||
on:input=move |e| amount.set(event_target_value(&e))
|
on:input=move |e| amount.set(event_target_value(&e))
|
||||||
|
|
@ -398,12 +396,12 @@ fn StageFertility(state: RwSignal<Option<State>>, stage: String) -> impl IntoVie
|
||||||
fn PersonEditor(p: Person, state: RwSignal<Option<State>>) -> impl IntoView {
|
fn PersonEditor(p: Person, state: RwSignal<Option<State>>) -> impl IntoView {
|
||||||
let id = p.0;
|
let id = p.0;
|
||||||
let fields = [
|
let fields = [
|
||||||
("hunger", p.4 as u32),
|
("hunger", format!("{:.0}", p.4.trunc())),
|
||||||
("food", p.5),
|
("food", p.5.to_string()),
|
||||||
("wood", p.6),
|
("wood", p.6.to_string()),
|
||||||
("stone", p.7),
|
("stone", p.7.to_string()),
|
||||||
]
|
]
|
||||||
.map(|(k, v)| (k, RwSignal::new(v.to_string())));
|
.map(|(k, v)| (k, RwSignal::new(v)));
|
||||||
view! {
|
view! {
|
||||||
<div>
|
<div>
|
||||||
{format!("Edit #{id}: ")}
|
{format!("Edit #{id}: ")}
|
||||||
|
|
@ -416,13 +414,14 @@ fn PersonEditor(p: Person, state: RwSignal<Option<State>>) -> impl IntoView {
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
prop:max=move || state.with(|s| s.as_ref().map(|s| {
|
prop:max=move || state.with(|s| s.as_ref().map_or_else(
|
||||||
if k == "hunger" {
|
|| "99".to_string(),
|
||||||
|
|s| if k == "hunger" {
|
||||||
s.limits.hunger.to_string()
|
s.limits.hunger.to_string()
|
||||||
} else {
|
} else {
|
||||||
s.limits.person_inventory.to_string()
|
s.limits.person_inventory.to_string()
|
||||||
}
|
},
|
||||||
}).unwrap_or_else(|| "99".to_string()))
|
))
|
||||||
prop:value=move || s.get()
|
prop:value=move || s.get()
|
||||||
on:input=move |e| s.set(event_target_value(&e))
|
on:input=move |e| s.set(event_target_value(&e))
|
||||||
/>
|
/>
|
||||||
|
|
@ -472,8 +471,10 @@ fn StockEditor(state: RwSignal<Option<State>>) -> impl IntoView {
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
prop:max=move || state.with(|s| {
|
prop:max=move || state.with(|s| {
|
||||||
s.as_ref().map(|s| s.limits.stock.to_string())
|
s.as_ref().map_or_else(
|
||||||
.unwrap_or_else(|| "9999".to_string())
|
|| "9999".to_string(),
|
||||||
|
|s| s.limits.stock.to_string(),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
prop:value=move || s.get()
|
prop:value=move || s.get()
|
||||||
on:input=move |e| s.set(event_target_value(&e))
|
on:input=move |e| s.set(event_target_value(&e))
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,25 @@
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
pub const T: f64 = 10.0;
|
/// Tile size in canvas pixels.
|
||||||
|
pub const TILE_PX: u32 = 10;
|
||||||
|
pub const T: f64 = TILE_PX as f64;
|
||||||
|
|
||||||
|
/// Canvas offset in pixels of tile coordinate `n`.
|
||||||
|
pub fn tile_offset(n: usize) -> f64 {
|
||||||
|
f64::from(u32::try_from(n).unwrap_or(u32::MAX)) * T
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Canvas length in pixels of `n` tiles.
|
||||||
|
pub fn canvas_len(n: usize) -> u32 {
|
||||||
|
u32::try_from(n).map_or(u32::MAX, |n| n.saturating_mul(TILE_PX))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tile index under canvas pixel offset `px`, clamped to `0..=max`.
|
||||||
|
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
||||||
|
pub fn tile_at(px: f64, max: usize) -> usize {
|
||||||
|
// Float-to-int `as` saturates and maps NaN to 0, so negative or huge offsets stay in range.
|
||||||
|
((px / T) as usize).min(max)
|
||||||
|
}
|
||||||
pub const TERRAIN: [&str; 7] = [
|
pub const TERRAIN: [&str; 7] = [
|
||||||
"Deep water",
|
"Deep water",
|
||||||
"Shallow water",
|
"Shallow water",
|
||||||
|
|
@ -114,8 +133,8 @@ impl Calendar {
|
||||||
return Some(format!("stage {stage} fertile {}", u8::from(value != 0)));
|
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" => (u32::try_from(value).unwrap_or(u32::MAX), range.max_days),
|
||||||
"max_days" => (range.min_days, value.min(u32::MAX as u64) as u32),
|
"max_days" => (range.min_days, u32::try_from(value).unwrap_or(u32::MAX)),
|
||||||
_ => return None,
|
_ => return None,
|
||||||
};
|
};
|
||||||
Some(format!("stage {stage} {min_days} {max_days}"))
|
Some(format!("stage {stage} {min_days} {max_days}"))
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue