1use flatland_protocol::{
4 BuildingView, DoorView, LifeState, ResourceNodeState, TerrainKindView, TerrainZoneView,
5 ZPlatformView, ZTransitionView,
6};
7
8use crate::mode::PathMode;
9
10pub const PLAYER_RADIUS_M: f32 = 0.45;
11pub const PATH_CLEARANCE_M: f32 = 0.35;
13pub const SEGMENT_SAMPLE_M: f32 = 0.3;
15pub const DEFAULT_COST: u16 = 10;
17
18#[derive(Debug, Clone, Copy)]
19pub struct NavBlockingCircle {
20 pub x: f32,
21 pub y: f32,
22 pub radius_m: f32,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq)]
27pub struct TerrainKindNavParams {
28 pub move_speed_mult: f32,
29 pub impassable: bool,
30}
31
32impl Default for TerrainKindNavParams {
33 fn default() -> Self {
34 Self {
35 move_speed_mult: 1.0,
36 impassable: false,
37 }
38 }
39}
40
41#[derive(Debug, Clone, Default)]
43pub struct TerrainNavTable {
44 params: Vec<(TerrainKindView, TerrainKindNavParams)>,
45}
46
47impl TerrainNavTable {
48 pub fn set(&mut self, kind: TerrainKindView, params: TerrainKindNavParams) {
49 if let Some(slot) = self.params.iter_mut().find(|(k, _)| *k == kind) {
50 slot.1 = params;
51 } else {
52 self.params.push((kind, params));
53 }
54 }
55
56 pub fn get(&self, kind: TerrainKindView) -> TerrainKindNavParams {
57 self.params
58 .iter()
59 .find(|(k, _)| *k == kind)
60 .map(|(_, p)| *p)
61 .unwrap_or_default()
62 }
63
64 pub fn iter(&self) -> impl Iterator<Item = (TerrainKindView, TerrainKindNavParams)> + '_ {
65 self.params.iter().copied()
66 }
67
68 pub fn unit_test_defaults() -> Self {
70 let mut t = Self::default();
71 let rows: &[(TerrainKindView, f32, bool)] = &[
72 (TerrainKindView::Grass, 1.0, false),
73 (TerrainKindView::Dirt, 0.98, false),
74 (TerrainKindView::Tilled, 0.95, false),
75 (TerrainKindView::Desert, 0.9, false),
76 (TerrainKindView::Hill, 0.92, false),
77 (TerrainKindView::Trail, 1.10, false),
78 (TerrainKindView::Road, 1.50, false),
79 (TerrainKindView::Rock, 0.85, true),
80 (TerrainKindView::Bog, 0.65, false),
81 (TerrainKindView::Beach, 0.94, false),
82 (TerrainKindView::ShallowWater, 0.55, false),
83 (TerrainKindView::DeepWater, 0.4, true),
84 ];
85 for &(kind, speed, impassable) in rows {
86 t.set(
87 kind,
88 TerrainKindNavParams {
89 move_speed_mult: speed,
90 impassable,
91 },
92 );
93 }
94 t
95 }
96}
97
98pub fn terrain_kind_view_from_id(id: &str) -> Option<TerrainKindView> {
100 Some(match id {
101 "grass" => TerrainKindView::Grass,
102 "dirt" => TerrainKindView::Dirt,
103 "tilled" => TerrainKindView::Tilled,
104 "desert" => TerrainKindView::Desert,
105 "hill" => TerrainKindView::Hill,
106 "bog" => TerrainKindView::Bog,
107 "beach" => TerrainKindView::Beach,
108 "shallow_water" => TerrainKindView::ShallowWater,
109 "deep_water" => TerrainKindView::DeepWater,
110 "trail" => TerrainKindView::Trail,
111 "road" => TerrainKindView::Road,
112 "rock" => TerrainKindView::Rock,
113 _ => return None,
114 })
115}
116
117#[derive(Debug, Clone)]
123pub struct StaticNavPaint {
124 pub width: i16,
125 pub height: i16,
126 pub kind_at: Vec<TerrainKindView>,
127 pub elev: Vec<f32>,
128 pub blocked_geometry: Vec<bool>,
130}
131
132#[derive(Debug, Clone)]
134pub struct NavWorld {
135 pub world_width_m: f32,
136 pub world_height_m: f32,
137 pub terrain_zones: Vec<TerrainZoneView>,
138 pub z_platforms: Vec<ZPlatformView>,
139 pub z_transitions: Vec<ZTransitionView>,
140 pub buildings: Vec<BuildingView>,
141 pub doors: Vec<DoorView>,
142 pub circles: Vec<NavBlockingCircle>,
143 pub kind_nav: TerrainNavTable,
145 static_paint: std::sync::Arc<std::sync::OnceLock<StaticNavPaint>>,
147}
148
149impl NavWorld {
150 pub fn new(
152 world_width_m: f32,
153 world_height_m: f32,
154 terrain_zones: Vec<TerrainZoneView>,
155 z_platforms: Vec<ZPlatformView>,
156 z_transitions: Vec<ZTransitionView>,
157 buildings: Vec<BuildingView>,
158 doors: Vec<DoorView>,
159 circles: Vec<NavBlockingCircle>,
160 kind_nav: TerrainNavTable,
161 ) -> Self {
162 Self {
163 world_width_m,
164 world_height_m,
165 terrain_zones,
166 z_platforms,
167 z_transitions,
168 buildings,
169 doors,
170 circles,
171 kind_nav,
172 static_paint: std::sync::Arc::new(std::sync::OnceLock::new()),
173 }
174 }
175
176 pub fn elevation_at(&self, x: f32, y: f32) -> f32 {
177 terrain_at(&self.terrain_zones, x, y)
178 .map(|z| z.elevation)
179 .unwrap_or(0.0)
180 }
181
182 pub fn terrain_kind_at(&self, x: f32, y: f32) -> TerrainKindView {
183 terrain_at(&self.terrain_zones, x, y)
184 .map(|z| z.kind)
185 .unwrap_or(TerrainKindView::Grass)
186 }
187
188 pub(crate) fn ensure_static_paint(
190 &self,
191 init: impl FnOnce() -> StaticNavPaint,
192 ) -> &StaticNavPaint {
193 self.static_paint.get_or_init(init)
194 }
195
196 pub fn adopt_static_paint_from(&mut self, other: &NavWorld) {
198 self.static_paint = std::sync::Arc::clone(&other.static_paint);
199 }
200
201 pub fn static_paint_ready(&self) -> bool {
203 self.static_paint.get().is_some()
204 }
205}
206
207fn terrain_at(zones: &[TerrainZoneView], x: f32, y: f32) -> Option<&TerrainZoneView> {
208 zones
210 .iter()
211 .enumerate()
212 .filter(|(_, zone)| x >= zone.x0 && x < zone.x1 && y >= zone.y0 && y < zone.y1)
213 .max_by(|(ia, a), (ib, b)| a.z_order.cmp(&b.z_order).then(ia.cmp(ib)))
214 .map(|(_, z)| z)
215}
216
217pub fn terrain_move_speed_mult(kind: TerrainKindView, table: &TerrainNavTable) -> f32 {
218 table.get(kind).move_speed_mult.max(0.01)
219}
220
221pub fn terrain_is_impassable(kind: TerrainKindView, table: &TerrainNavTable) -> bool {
222 table.get(kind).impassable
223}
224
225pub fn terrain_cost(kind: TerrainKindView, mode: PathMode, table: &TerrainNavTable) -> u16 {
227 if terrain_is_impassable(kind, table) {
228 return u16::MAX;
229 }
230 match mode {
231 PathMode::Direct => DEFAULT_COST,
232 PathMode::Fastest => {
233 let speed = terrain_move_speed_mult(kind, table);
234 let c = (DEFAULT_COST as f32 / speed).round();
235 c.clamp(1.0, (u16::MAX - 1) as f32) as u16
236 }
237 }
238}
239
240pub fn min_walkable_cost(
242 mode: PathMode,
243 zones: &[TerrainZoneView],
244 table: &TerrainNavTable,
245) -> u16 {
246 match mode {
247 PathMode::Direct => DEFAULT_COST,
248 PathMode::Fastest => {
249 let mut min_c = DEFAULT_COST;
250 for z in zones {
251 let c = terrain_cost(z.kind, PathMode::Fastest, table);
252 if c != u16::MAX && c < min_c {
253 min_c = c;
254 }
255 }
256 min_c
257 }
258 }
259}
260
261pub fn collides_player_at(x: f32, y: f32, world: &NavWorld) -> bool {
262 if world
263 .circles
264 .iter()
265 .any(|o| circle_overlap(x, y, PLAYER_RADIUS_M, o.x, o.y, o.radius_m))
266 {
267 return true;
268 }
269 let pad = PLAYER_RADIUS_M;
270 world.buildings.iter().any(|b| {
271 let hw = b.width_m / 2.0 + pad;
272 let hd = b.depth_m / 2.0 + pad;
273 x >= b.x - hw && x <= b.x + hw && y >= b.y - hd && y <= b.y + hd
274 })
275}
276
277fn circle_overlap(ax: f32, ay: f32, ar: f32, bx: f32, by: f32, br: f32) -> bool {
278 let dx = ax - bx;
279 let dy = ay - by;
280 let min_dist = ar + br;
281 dx * dx + dy * dy < min_dist * min_dist
282}
283
284pub(crate) fn block_circle(
285 blocked: &mut [bool],
286 width: i16,
287 height: i16,
288 cx: f32,
289 cy: f32,
290 radius_m: f32,
291) {
292 let block_r = radius_m + PLAYER_RADIUS_M + PATH_CLEARANCE_M;
293 let r = block_r.ceil() as i16;
294 let ix = cx.floor() as i16;
295 let iy = cy.floor() as i16;
296 for dy in -r..=r {
297 for dx in -r..=r {
298 let cell_x = ix + dx;
299 let cell_y = iy + dy;
300 if cell_x < 0 || cell_y < 0 || cell_x >= width || cell_y >= height {
301 continue;
302 }
303 let cell_cx = cell_x as f32 + 0.5;
304 let cell_cy = cell_y as f32 + 0.5;
305 if (cell_cx - cx).hypot(cell_cy - cy) <= block_r {
306 let idx = (cell_y as usize) * (width as usize) + (cell_x as usize);
307 blocked[idx] = true;
308 }
309 }
310 }
311}
312
313pub(crate) fn mark_building_footprint(
314 blocked: &mut [bool],
315 width: i16,
316 height: i16,
317 building: &BuildingView,
318) {
319 let pad = PLAYER_RADIUS_M;
323 let hw = building.width_m / 2.0;
324 let hd = building.depth_m / 2.0;
325 let x0 = (building.x - hw - pad).floor() as i16;
326 let y0 = (building.y - hd - pad).floor() as i16;
327 let x1 = (building.x + hw + pad).ceil() as i16 - 1;
328 let y1 = (building.y + hd + pad).ceil() as i16 - 1;
329 if x1 < x0 || y1 < y0 {
330 return;
331 }
332 for x in x0..=x1 {
333 for y in y0..=y1 {
334 if x >= 0 && y >= 0 && x < width && y < height {
335 let idx = (y as usize) * (width as usize) + (x as usize);
336 blocked[idx] = true;
337 }
338 }
339 }
340}
341
342pub(crate) fn clear_door_cells(blocked: &mut [bool], width: i16, height: i16, doors: &[DoorView]) {
343 for door in doors {
344 if door.open {
345 let (x, y) = world_to_cell(door.x, door.y);
346 for dx in -1i16..=1 {
347 for dy in -1i16..=1 {
348 if dx.abs() + dy.abs() <= 1 {
349 let cx = x + dx;
350 let cy = y + dy;
351 if cx >= 0 && cy >= 0 && cx < width && cy < height {
352 let idx = (cy as usize) * (width as usize) + (cx as usize);
353 blocked[idx] = false;
354 }
355 }
356 }
357 }
358 }
359 }
360}
361
362pub(crate) fn world_to_cell(x: f32, y: f32) -> (i16, i16) {
363 (x.floor() as i16, y.floor() as i16)
364}
365
366pub(crate) fn cell_center(x: i16, y: i16) -> (f32, f32) {
367 (x as f32 + 0.5, y as f32 + 0.5)
368}
369
370pub const DEFAULT_PLACED_PROP_BLOCK_RADIUS_M: f32 = 0.7;
372
373pub fn circles_from_placed_containers(
375 containers: &[flatland_protocol::PlacedContainerView],
376) -> Vec<NavBlockingCircle> {
377 containers
378 .iter()
379 .filter(|c| c.blocking)
380 .map(|c| {
381 let radius_m = if c.blocking_radius_m > 0.0 {
382 c.blocking_radius_m
383 } else {
384 DEFAULT_PLACED_PROP_BLOCK_RADIUS_M
385 };
386 NavBlockingCircle {
387 x: c.x,
388 y: c.y,
389 radius_m,
390 }
391 })
392 .collect()
393}
394
395pub fn snap_nav_goal(world: &NavWorld, gx: f32, gy: f32) -> (f32, f32) {
397 if !nav_position_blocked(world, gx, gy, PATH_CLEARANCE_M) {
398 return (gx, gy);
399 }
400 for step in 1..=40 {
401 let d = step as f32 * 0.35;
402 for (dx, dy) in [
403 (0.0, -d),
404 (0.0, d),
405 (d, 0.0),
406 (-d, 0.0),
407 (d, -d),
408 (d, d),
409 (-d, -d),
410 (-d, d),
411 ] {
412 let nx = gx + dx;
413 let ny = gy + dy;
414 if !nav_position_blocked(world, nx, ny, PATH_CLEARANCE_M) {
415 return (nx, ny);
416 }
417 }
418 }
419 (gx, gy)
420}
421
422fn nav_position_blocked(world: &NavWorld, x: f32, y: f32, extra_pad_m: f32) -> bool {
423 let body = PLAYER_RADIUS_M + extra_pad_m.max(0.0);
424 if world
425 .circles
426 .iter()
427 .any(|o| circle_overlap(x, y, body, o.x, o.y, o.radius_m + extra_pad_m.max(0.0)))
428 {
429 return true;
430 }
431 world.buildings.iter().any(|b| {
432 let hw = b.width_m / 2.0 + body;
433 let hd = b.depth_m / 2.0 + body;
434 x >= b.x - hw && x <= b.x + hw && y >= b.y - hd && y <= b.y + hd
435 })
436}
437
438pub fn circles_from_views(
440 resource_nodes: &[flatland_protocol::ResourceNodeView],
441 npcs: &[flatland_protocol::NpcView],
442) -> Vec<NavBlockingCircle> {
443 let mut circles = Vec::new();
444 for node in resource_nodes {
445 if node.blocking
446 && matches!(
447 node.state,
448 ResourceNodeState::Available | ResourceNodeState::Harvesting
449 )
450 {
451 let radius = if node.blocking_radius_m > 0.0 {
452 node.blocking_radius_m
453 } else {
454 0.8
455 };
456 circles.push(NavBlockingCircle {
457 x: node.x,
458 y: node.y,
459 radius_m: radius,
460 });
461 }
462 }
463 for npc in npcs {
464 let alive = npc.life_state != Some(LifeState::Dead) && npc.hp_pct.is_none_or(|h| h > 0.0);
465 if alive {
466 circles.push(NavBlockingCircle {
467 x: npc.x,
468 y: npc.y,
469 radius_m: 0.55,
470 });
471 }
472 }
473 circles
474}