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
9
build.rs
9
build.rs
|
|
@ -35,11 +35,10 @@ fn main() {
|
|||
let status = cargo
|
||||
.status()
|
||||
.expect("failed to run cargo for the ui crate");
|
||||
if !status.success() {
|
||||
panic!(
|
||||
"building ui/ failed (see the cargo output above); the wasm32-unknown-unknown target must be installed: rustup target add wasm32-unknown-unknown"
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
status.success(),
|
||||
"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 wasm = target_dir
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ pub fn connect(on_state: impl Fn(String) + 'static) {
|
|||
"ws"
|
||||
};
|
||||
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 {
|
||||
loop {
|
||||
|
|
@ -45,8 +45,8 @@ pub fn connect(on_state: impl Fn(String) + 'static) {
|
|||
});
|
||||
}
|
||||
|
||||
fn open(url: String, on_state: Rc<dyn Fn(String)>) {
|
||||
let Ok(ws) = WebSocket::new(&url) else { return };
|
||||
fn open(url: &str, on_state: Rc<dyn Fn(String)>) {
|
||||
let Ok(ws) = WebSocket::new(url) else { return };
|
||||
let cb = on_state.clone();
|
||||
let onmessage = Closure::<dyn FnMut(MessageEvent)>::new(move |e: MessageEvent| {
|
||||
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()));
|
||||
onmessage.forget();
|
||||
|
||||
let url2 = url.clone();
|
||||
let url = url.to_string();
|
||||
let onclose = Closure::<dyn FnMut()>::new(move || {
|
||||
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 {
|
||||
TimeoutFuture::new(1000).await;
|
||||
open(url, on_state);
|
||||
open(&url, on_state);
|
||||
});
|
||||
});
|
||||
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 web_sys::{CanvasRenderingContext2d, HtmlCanvasElement};
|
||||
|
||||
|
|
@ -7,8 +7,8 @@ const COLORS: [&str; 7] = [
|
|||
];
|
||||
|
||||
pub fn draw(canvas: &HtmlCanvasElement, t: &Terrain, s: &State, sel: Option<Sel>) {
|
||||
canvas.set_width((t.w as f64 * T) as u32);
|
||||
canvas.set_height((t.h as f64 * T) as u32);
|
||||
canvas.set_width(canvas_len(t.w));
|
||||
canvas.set_height(canvas_len(t.h));
|
||||
let Some(ctx) = canvas
|
||||
.get_context("2d")
|
||||
.ok()
|
||||
|
|
@ -21,7 +21,7 @@ pub fn draw(canvas: &HtmlCanvasElement, t: &Terrain, s: &State, sel: Option<Sel>
|
|||
for y in 0..t.h {
|
||||
for x in 0..t.w {
|
||||
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.begin_path();
|
||||
let _ = ctx.arc(
|
||||
x as f64 * T + T / 2.0,
|
||||
y as f64 * T + T / 2.0,
|
||||
tile_offset(x) + T / 2.0,
|
||||
tile_offset(y) + T / 2.0,
|
||||
r,
|
||||
0.0,
|
||||
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"),
|
||||
_ => {
|
||||
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");
|
||||
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.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 {
|
||||
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.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 {
|
||||
let (x0, y0, x1, y1) = sel.rect();
|
||||
ctx.set_stroke_style_str("#fff");
|
||||
ctx.set_line_width(2.0);
|
||||
ctx.stroke_rect(
|
||||
x0 as f64 * T,
|
||||
y0 as f64 * T,
|
||||
(x1 - x0 + 1) as f64 * T,
|
||||
(y1 - y0 + 1) as f64 * T,
|
||||
tile_offset(x0),
|
||||
tile_offset(y0),
|
||||
tile_offset(x1 - x0 + 1),
|
||||
tile_offset(y1 - y0 + 1),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ mod model;
|
|||
use leptos::mount::mount_to_body;
|
||||
use leptos::prelude::*;
|
||||
use leptos::task::spawn_local;
|
||||
use model::{Person, Sel, State, T, Terrain};
|
||||
use model::{Person, Sel, State, Terrain, tile_at};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[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()) {
|
||||
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.
|
||||
let r = c.get_bounding_client_rect();
|
||||
let (sx, sy) = (
|
||||
c.width() as f64 / r.width().max(1.0),
|
||||
c.height() as f64 / r.height().max(1.0),
|
||||
f64::from(c.width()) / r.width().max(1.0),
|
||||
f64::from(c.height()) / r.height().max(1.0),
|
||||
);
|
||||
let px = (ev.client_x() as f64 - r.left()) * sx;
|
||||
let py = (ev.client_y() as f64 - r.top()) * sy;
|
||||
let px = (f64::from(ev.client_x()) - r.left()) * sx;
|
||||
let py = (f64::from(ev.client_y()) - r.top()) * sy;
|
||||
terrain.with_untracked(|t| {
|
||||
t.as_ref().map(|t| {
|
||||
let x = ((px / T).max(0.0) as usize).min(t.w - 1);
|
||||
let y = ((py / T).max(0.0) as usize).min(t.h - 1);
|
||||
(x, y)
|
||||
})
|
||||
t.as_ref()
|
||||
.map(|t| (tile_at(px, t.w - 1), tile_at(py, t.h - 1)))
|
||||
})
|
||||
};
|
||||
|
||||
|
|
@ -149,10 +146,10 @@ fn App() -> impl IntoView {
|
|||
<div class="side">
|
||||
<h3>"World"</h3>
|
||||
{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! {
|
||||
<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
|
||||
)}</div>
|
||||
<div>{format!("People: {}", s.people.len())}</div>
|
||||
|
|
@ -181,7 +178,7 @@ fn App() -> impl IntoView {
|
|||
}))}
|
||||
<div>
|
||||
<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>
|
||||
" Speed "
|
||||
<select
|
||||
|
|
@ -278,13 +275,14 @@ fn App() -> impl IntoView {
|
|||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
prop:max=move || state.with(|s| s.as_ref().map(|s| {
|
||||
match tool.get().as_str() {
|
||||
prop:max=move || state.with(|s| s.as_ref().map_or_else(
|
||||
|| "100".to_string(),
|
||||
|s| match tool.get().as_str() {
|
||||
"energy" => s.limits.animal_energy.to_string(),
|
||||
"spawn" => s.limits.spawn.to_string(),
|
||||
_ => s.limits.resource.to_string(),
|
||||
}
|
||||
}).unwrap_or_else(|| "100".to_string()))
|
||||
},
|
||||
))
|
||||
title="amount / energy"
|
||||
prop:value=move || amount.get()
|
||||
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 {
|
||||
let id = p.0;
|
||||
let fields = [
|
||||
("hunger", p.4 as u32),
|
||||
("food", p.5),
|
||||
("wood", p.6),
|
||||
("stone", p.7),
|
||||
("hunger", format!("{:.0}", p.4.trunc())),
|
||||
("food", p.5.to_string()),
|
||||
("wood", p.6.to_string()),
|
||||
("stone", p.7.to_string()),
|
||||
]
|
||||
.map(|(k, v)| (k, RwSignal::new(v.to_string())));
|
||||
.map(|(k, v)| (k, RwSignal::new(v)));
|
||||
view! {
|
||||
<div>
|
||||
{format!("Edit #{id}: ")}
|
||||
|
|
@ -416,13 +414,14 @@ fn PersonEditor(p: Person, state: RwSignal<Option<State>>) -> impl IntoView {
|
|||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
prop:max=move || state.with(|s| s.as_ref().map(|s| {
|
||||
if k == "hunger" {
|
||||
prop:max=move || state.with(|s| s.as_ref().map_or_else(
|
||||
|| "99".to_string(),
|
||||
|s| if k == "hunger" {
|
||||
s.limits.hunger.to_string()
|
||||
} else {
|
||||
s.limits.person_inventory.to_string()
|
||||
}
|
||||
}).unwrap_or_else(|| "99".to_string()))
|
||||
},
|
||||
))
|
||||
prop:value=move || s.get()
|
||||
on:input=move |e| s.set(event_target_value(&e))
|
||||
/>
|
||||
|
|
@ -472,8 +471,10 @@ fn StockEditor(state: RwSignal<Option<State>>) -> impl IntoView {
|
|||
type="number"
|
||||
min="0"
|
||||
prop:max=move || state.with(|s| {
|
||||
s.as_ref().map(|s| s.limits.stock.to_string())
|
||||
.unwrap_or_else(|| "9999".to_string())
|
||||
s.as_ref().map_or_else(
|
||||
|| "9999".to_string(),
|
||||
|s| s.limits.stock.to_string(),
|
||||
)
|
||||
})
|
||||
prop:value=move || s.get()
|
||||
on:input=move |e| s.set(event_target_value(&e))
|
||||
|
|
|
|||
|
|
@ -1,6 +1,25 @@
|
|||
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] = [
|
||||
"Deep water",
|
||||
"Shallow water",
|
||||
|
|
@ -114,8 +133,8 @@ impl Calendar {
|
|||
return Some(format!("stage {stage} fertile {}", u8::from(value != 0)));
|
||||
}
|
||||
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),
|
||||
"min_days" => (u32::try_from(value).unwrap_or(u32::MAX), range.max_days),
|
||||
"max_days" => (range.min_days, u32::try_from(value).unwrap_or(u32::MAX)),
|
||||
_ => return None,
|
||||
};
|
||||
Some(format!("stage {stage} {min_days} {max_days}"))
|
||||
|
|
|
|||
Loading…
Reference in New Issue