Skip to main content

veloren_world/site/plot/
airship_dock.rs

1use super::*;
2use crate::{
3    Land,
4    site::generation::{PrimitiveTransform, spiral_staircase},
5    util::{CARDINALS, DIAGONALS, RandomField, Sampler, within_distance},
6};
7use common::{
8    generation::SpecialEntity,
9    terrain::{BlockKind, SpriteKind},
10};
11use rand::prelude::*;
12use std::{f32::consts::PI, sync::Arc};
13use vek::{ops::Lerp, *};
14
15/// Represents house data generated by the `generate()` method
16pub struct AirshipDock {
17    /// Approximate altitude of the door tile
18    pub(crate) alt: i32,
19    rotation: f32,
20    pub door_tile: Vec2<i32>,
21    pub center: Vec2<i32>,
22    base: i32,
23    upper_alt: i32,
24    min_foundation_alt: i32,
25    surface_colors: Vec<Rgb<u8>>,
26    sub_surface_colors: Vec<Rgb<u8>>,
27    pub docking_positions: Vec<Vec3<i32>>,
28    pub door_dir: Vec2<i32>,
29    campfire_pos: Vec3<i32>,
30}
31
32impl AirshipDock {
33    pub fn generate(
34        land: &Land,
35        index: IndexRef,
36        _rng: &mut impl Rng,
37        site: &Site,
38        door_tile: Vec2<i32>,
39        door_dir: Vec2<i32>,
40        tile_aabr: Aabr<i32>,
41    ) -> Self {
42        // dock is 30 blocks in radius
43        // airships are 37 blocks wide.
44        // distance from the center to the outside edge of the airship when docked is 67
45        // blocks. The area covered by all four airships is a square 134 blocks
46        // on a side.
47
48        let door_tile_pos: Vec2<i32> = site.tile_center_wpos(door_tile);
49        let bounds = Aabr {
50            min: site.tile_wpos(tile_aabr.min),
51            max: site.tile_wpos(tile_aabr.max),
52        };
53        let center = bounds.center();
54        // door_tile_pos and tile_aabr are relative to the site origin.
55        // bounds and center are in world blocks.
56
57        // The bounds will be 54 blocks square.
58        // That's enough for the dock structure itself minus 1/2 an overlap tile on each
59        // side. In city towns with low buildings, this allows airships to dock
60        // above the buildings.
61
62        // For airship clearance however, the terrain must be sampled across the entire
63        // area where an airship might be docked (134 blocks square), plus some
64        // extra to allow for when the airship overshoots the dock on descent.
65        let halfspan = 66;
66        let min_clearance_x = center.x - halfspan;
67        let max_clearance_x = center.x + halfspan;
68        let min_clearance_y = center.y - halfspan;
69        let max_clearance_y = center.y + halfspan;
70        let mut max_surface_alt = i32::MIN;
71        let mut surface_colors = vec![];
72        let mut sub_surface_colors = vec![];
73        // Since this range is from -66 to +66 (132 blocks), sampling every 22 blocks
74        // will give exactly 7 samples along each axis (because 132 is divisible by 22)
75        // as long as the range is inclusive.
76        for x in (min_clearance_x..=max_clearance_x).step_by(22) {
77            for y in (min_clearance_y..=max_clearance_y).step_by(22) {
78                let pos = Vec2::new(x, y);
79                let alt = land.get_surface_alt_approx(pos) as i32;
80                if alt > max_surface_alt {
81                    max_surface_alt = alt;
82                }
83            }
84        }
85
86        // The foundation and possibly pedestal that the tower sits on
87        // is approximately 40 blocks square. The bottom of the foundation area
88        // must be at the lowest surface altitude in that area.
89        let min_foundation_x = center.x - 20;
90        let max_foundation_x = center.x + 20;
91        let min_foundation_y = center.y - 20;
92        let max_foundation_y = center.y + 20;
93        let mut max_foundation_alt = i32::MIN;
94        let mut min_foundation_alt = i32::MAX;
95        let color_component_to_u8 =
96            |compf32: f32| -> u8 { (compf32.clamp(0.0, 1.0) * 255.0).floor() as u8 };
97
98        // Since this range is from -20 to +20 (40 blocks), sampling every 10 blocks
99        // will give exactly 5 samples along each axis (because 40 is divisible by 10)
100        // as long as the range is inclusive.
101        for x in (min_foundation_x..=max_foundation_x).step_by(10) {
102            for y in (min_foundation_y..=max_foundation_y).step_by(10) {
103                let pos = Vec2::new(x, y);
104                let alt = land.get_surface_alt_approx(pos) as i32;
105                if alt > max_foundation_alt {
106                    max_foundation_alt = alt;
107                }
108                if alt < min_foundation_alt {
109                    min_foundation_alt = alt;
110                }
111                if let Some(sample) = land.column_sample(pos, index) {
112                    surface_colors.push(sample.surface_color.map(color_component_to_u8));
113                    sub_surface_colors.push(sample.sub_surface_color.map(color_component_to_u8));
114                }
115            }
116        }
117        // When docked, the bottom of the airship will be at the platform height.
118        // The platform height must be at or above max_surface_alt.
119        // The platform height must be at least 36 blocks above the base of the tower
120        // (the tower must be at least 36 blocks tall).
121        // The base of the tower must be at or above max_foundation_alt.
122        let min_base = max_surface_alt - 36;
123        let base = min_base.max(max_foundation_alt);
124
125        let upper_alt = base + 28;
126        // negate the rotation angle because Mat3::rotation_z is CCW, but atan2 returns
127        // PI/2 for (1,0) which is a CW rotation.
128        let rotation = -f32::atan2(door_dir.x as f32, door_dir.y as f32);
129        let mut docking_positions = vec![];
130        for dir in CARDINALS {
131            let pos = (center + dir * 31).with_z(upper_alt + 9);
132            docking_positions.push(pos);
133        }
134        let campfire_dir = Mat2::rotation_z(-PI / 4.0) * door_dir.map(|i| i as f32);
135        let campfire_pos = (center.map(|i| i as f32) + (campfire_dir * 11.0))
136            .map(|f| f.round() as i32)
137            .with_z(upper_alt + 9);
138        Self {
139            door_tile: door_tile_pos,
140            alt: base,
141            rotation,
142            center,
143            base,
144            upper_alt,
145            min_foundation_alt,
146            surface_colors,
147            sub_surface_colors,
148            docking_positions,
149            door_dir,
150            campfire_pos,
151        }
152    }
153}
154
155impl Structure for AirshipDock {
156    #[cfg(feature = "dyn-lib")]
157    #[unsafe(export_name = "as_dyn_structure_airshipdock")]
158    fn as_dyn_outer(&self) -> Option<(&dyn Structure, &'static str)> {
159        Some((Self::as_dyn_impl(self), "as_dyn_structure_airshipdock"))
160    }
161
162    fn spawn_rules_inner(
163        &self,
164        spawn_rules: &mut SpawnRules,
165        _land: &Land,
166        wpos: Vec2<i32>,
167        _weight: f32,
168    ) {
169        // dock is 5 tiles = 30 blocks in radius
170        // airships are 37 blocks wide.
171        // Some trees are 20 to 30 blocks in radius.
172        // Leave extra space for tree width.
173        // Don't allow trees within 30 + 37 + 30 = 97 blocks of the dock center
174        const AIRSHIP_MIN_TREE_DIST2: i32 = 100;
175        spawn_rules.trees &= !within_distance(wpos, self.center, AIRSHIP_MIN_TREE_DIST2);
176
177        const MIN_FLAT_DIST: f32 = 12.0;
178        const SLOPE_LENGTH: f32 = 16.0;
179        let dist = wpos.as_::<f32>().distance(self.center.as_());
180        let weight = (1.0 - (dist - MIN_FLAT_DIST).max(0.0) / SLOPE_LENGTH).max(0.0);
181        spawn_rules.prefer_alt(self.alt as f32, weight);
182    }
183
184    fn airship_dock_info(&self) -> Option<AirshipDockInfo<'_>> {
185        Some(AirshipDockInfo {
186            door_tile: self.door_tile,
187            center: self.center,
188            docking_positions: &self.docking_positions,
189        })
190    }
191
192    fn render_inner(&self, _site: &Site, _land: &Land, painter: &Painter) {
193        let brick = Fill::Brick(BlockKind::Rock, Rgb::new(80, 75, 85), 24);
194        let wood = Fill::Brick(BlockKind::Rock, Rgb::new(45, 28, 21), 24);
195        let woodalt = Fill::Brick(BlockKind::Rock, Rgb::new(30, 22, 15), 24);
196        let grass = {
197            let surface_colors = self.surface_colors.clone();
198            let num_colors = surface_colors.len() as u32;
199            Fill::Sampling(Arc::new(move |wpos| {
200                let index = if num_colors > 0 {
201                    (RandomField::new(0).get(wpos) % num_colors) as usize
202                } else {
203                    0usize
204                };
205                Some(Block::new(
206                    BlockKind::Earth,
207                    surface_colors
208                        .get(index)
209                        .cloned()
210                        .unwrap_or(Rgb::new(55, 25, 8)),
211                ))
212            }))
213        };
214        let dirt = {
215            let sub_surface_colors = self.sub_surface_colors.clone();
216            let num_colors = sub_surface_colors.len() as u32;
217            let avg_color = if num_colors > 0 {
218                let mut r_total: f32 = 0.0;
219                let mut g_total: f32 = 0.0;
220                let mut b_total: f32 = 0.0;
221                for color in &sub_surface_colors {
222                    r_total += color.r as f32;
223                    g_total += color.g as f32;
224                    b_total += color.b as f32;
225                }
226                Rgb::new(
227                    r_total / num_colors as f32,
228                    g_total / num_colors as f32,
229                    b_total / num_colors as f32,
230                )
231            } else {
232                Rgb::new(55.0, 25.0, 8.0)
233            };
234            Fill::Sampling(Arc::new(move |wpos| {
235                let color = if num_colors > 0 {
236                    sub_surface_colors
237                        .get((RandomField::new(0).get(wpos) % num_colors) as usize)
238                        .copied()
239                        .unwrap_or(Rgb::new(55, 25, 8))
240                        .map(|u| u as f32)
241                } else {
242                    avg_color
243                };
244                // the final color is a lerp from avg_color 25% to 75% of the way to the
245                // randomly selected color index.
246                let lerp_factor = (RandomField::new(1).get(wpos) % 51 + 25) as f32 / 100.0;
247                let block_color = Rgb::new(
248                    Lerp::lerp(avg_color.r, color.r, lerp_factor),
249                    Lerp::lerp(avg_color.g, color.g, lerp_factor),
250                    Lerp::lerp(avg_color.b, color.b, lerp_factor),
251                )
252                .map(|f| f.clamp(0.0, 255.0) as u8);
253                Some(Block::new(BlockKind::Earth, block_color))
254            }))
255        };
256        let base = self.base;
257        let center = self.center;
258        let upper_alt = self.upper_alt;
259        let min_foundation_alt = self.min_foundation_alt;
260
261        // Build everything that is outside the column that
262        // forms the main structure before the tower so that
263        // painting the main column will carve a hole inside them.
264
265        //bracing
266        painter
267            .cylinder_with_radius(center.with_z(upper_alt - 3), 7.0, 1.0)
268            .fill(wood.clone());
269        painter
270            .cylinder_with_radius(center.with_z(upper_alt + 6), 7.0, 1.0)
271            .fill(wood.clone());
272        painter
273            .cylinder_with_radius(center.with_z(upper_alt + 7), 8.0, 1.0)
274            .fill(wood.clone());
275
276        // platform
277        painter
278            .superquadric(
279                Aabb {
280                    min: (center - 8000).with_z(upper_alt + 7),
281                    max: (center + 8000).with_z(upper_alt + 11),
282                },
283                0.3,
284            )
285            .intersect(painter.aabb(Aabb {
286                min: (center - 31).with_z(upper_alt + 7),
287                max: (center + 31).with_z(upper_alt + 11),
288            }))
289            .fill(woodalt.clone());
290        painter
291            .cylinder_with_radius(center.with_z(upper_alt + 8), 19.0, 2.0)
292            .fill(woodalt.clone());
293        painter
294            .cylinder_with_radius(center.with_z(upper_alt + 8), 18.0, 2.0)
295            .fill(wood.clone());
296        painter
297            .cylinder_with_radius(center.with_z(upper_alt + 9), 18.0, 1.0)
298            .clear();
299        painter
300            .aabb(Aabb {
301                min: Vec2::new(center.x - 30, center.y - 2).with_z(upper_alt + 8),
302                max: Vec2::new(center.x + 30, center.y + 2).with_z(upper_alt + 10),
303            })
304            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
305            .fill(wood.clone());
306        painter
307            .aabb(Aabb {
308                min: Vec2::new(center.x - 30, center.y - 2).with_z(upper_alt + 9),
309                max: Vec2::new(center.x + 30, center.y + 2).with_z(upper_alt + 10),
310            })
311            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
312            .clear();
313        painter
314            .aabb(Aabb {
315                min: Vec2::new(center.x - 2, center.y - 30).with_z(upper_alt + 8),
316                max: Vec2::new(center.x + 2, center.y + 30).with_z(upper_alt + 10),
317            })
318            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
319            .fill(wood.clone());
320        painter
321            .aabb(Aabb {
322                min: Vec2::new(center.x - 2, center.y - 30).with_z(upper_alt + 9),
323                max: Vec2::new(center.x + 2, center.y + 30).with_z(upper_alt + 10),
324            })
325            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
326            .clear();
327
328        // Agent desk
329        painter
330            .cylinder_with_radius(center.with_z(upper_alt + 9), 9.0, 6.0)
331            .intersect(painter.aabb(Aabb {
332                min: (Vec2::new(center.x + 2, center.y - 2)).with_z(upper_alt + 9),
333                max: (Vec2::new(center.x + 40, center.y - 40)).with_z(upper_alt + 16),
334            }))
335            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
336            .fill(woodalt.clone());
337        // carve out the middle
338        painter
339            .cylinder_with_radius(center.with_z(upper_alt + 9), 9.0, 5.0)
340            .intersect(painter.aabb(Aabb {
341                min: (Vec2::new(center.x + 3, center.y - 3)).with_z(upper_alt + 9),
342                max: (Vec2::new(center.x + 40, center.y - 40)).with_z(upper_alt + 15),
343            }))
344            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
345            .clear();
346        // Desk radius
347        painter
348            .cylinder_with_radius(center.with_z(upper_alt + 9), 9.0, 1.0)
349            .intersect(painter.aabb(Aabb {
350                min: (Vec2::new(center.x + 3, center.y - 3)).with_z(upper_alt + 9),
351                max: (Vec2::new(center.x + 40, center.y - 40)).with_z(upper_alt + 11),
352            }))
353            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
354            .fill(woodalt.clone());
355        // Clear inner floor
356        painter
357            .cylinder_with_radius(center.with_z(upper_alt + 9), 8.0, 1.0)
358            .intersect(painter.aabb(Aabb {
359                min: (Vec2::new(center.x + 3, center.y - 3)).with_z(upper_alt + 9),
360                max: (Vec2::new(center.x + 40, center.y - 40)).with_z(upper_alt + 11),
361            }))
362            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
363            .clear();
364        // Clear extra wall extensions
365        painter
366            .line(
367                Vec2::new(center.x + 2, center.y - 9).with_z(upper_alt + 9),
368                Vec2::new(center.x + 2, center.y - 9).with_z(upper_alt + 16),
369                0.5,
370            )
371            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
372            .clear();
373        painter
374            .line(
375                Vec2::new(center.x + 8, center.y - 3).with_z(upper_alt + 9),
376                Vec2::new(center.x + 8, center.y - 3).with_z(upper_alt + 16),
377                0.5,
378            )
379            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
380            .clear();
381
382        let foundation_radius: f32 = 17.0;
383
384        // The rampart court is below the base except for a low stone wall at the top.
385        // Build the low stone wall before the main column.
386        painter
387            .cylinder_with_radius(center.with_z(base - 1), foundation_radius, 2.0)
388            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
389            .fill(brick.clone());
390        painter
391            .cylinder_with_radius(center.with_z(base - 1), foundation_radius - 1.0, 2.0)
392            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
393            .clear();
394
395        //lower doorway
396        painter
397            .cylinder_with_radius(
398                Vec2::new(center.x - 1, center.y + 12).with_z(base - 5),
399                4.5,
400                7.0,
401            )
402            .rotate_about_min(Mat3::new(1, 0, 0, 0, 0, -1, 0, 1, 0))
403            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
404            .fill(wood.clone());
405
406        // The main dock structure is a brick column.
407
408        //column
409        painter
410            .cylinder_with_radius(center.with_z(base), 6.0, 45.0)
411            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
412            .fill(brick.clone());
413
414        // ring at floor level outside the column
415        painter
416            .cylinder_with_radius(center.with_z(base + 35), 8.0, 2.0)
417            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
418            .fill(brick.clone());
419        // half ring with offset to exclude the agent area floor
420        painter
421            .cylinder_with_radius(center.with_z(upper_alt + 9), 7.0, 1.0)
422            .intersect(painter.aabb(Aabb {
423                min: (Vec2::new(center.x + 2, center.y - 8)).with_z(upper_alt + 9),
424                max: (Vec2::new(center.x - 8, center.y + 8)).with_z(upper_alt + 10),
425            }))
426            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
427            .fill(brick.clone());
428        // line under doorway
429        painter
430            .line(
431                Vec2::new(center.x + 6, center.y - 2).with_z(upper_alt + 9),
432                Vec2::new(center.x + 6, center.y + 2).with_z(upper_alt + 9),
433                0.5,
434            )
435            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
436            .fill(brick.clone());
437
438        //lower doorway cut
439        painter
440            .cylinder_with_radius(
441                Vec2::new(center.x - 1, center.y + 12).with_z(base - 5),
442                3.5,
443                7.0,
444            )
445            .rotate_about_min(Mat3::new(1, 0, 0, 0, 0, -1, 0, 1, 0))
446            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
447            .clear();
448
449        // finally, clear the entire inside of the tower
450        painter
451            .cylinder_with_radius(center.with_z(base), 5.0, 45.0)
452            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
453            .clear();
454
455        // The top of the tower
456
457        //cone
458        painter
459            .cone_with_radius(center.with_z(base + 45), 8.0, 18.0)
460            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
461            .fill(wood.clone());
462        //remove 1/4 cyl
463        painter
464            .aabb(Aabb {
465                min: Vec2::new(center.x - 1, center.y + 1).with_z(upper_alt + 9),
466                max: Vec2::new(center.x + 6, center.y + 6).with_z(upper_alt + 17),
467            })
468            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
469            .fill(brick.clone());
470        painter
471            .aabb(Aabb {
472                min: Vec2::new(center.x, center.y + 2).with_z(upper_alt + 9),
473                max: Vec2::new(center.x + 6, center.y + 7).with_z(upper_alt + 17),
474            })
475            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
476            .clear();
477        //platform cleanup
478        painter
479            .aabb(Aabb {
480                min: Vec2::new(center.x - 2, center.y - 15).with_z(upper_alt + 8),
481                max: Vec2::new(center.x + 6, center.y + 9).with_z(upper_alt + 9),
482            })
483            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
484            .fill(wood.clone());
485
486        //upper door
487        painter
488            .aabb(Aabb {
489                min: Vec2::new(center.x + 5, center.y - 2).with_z(upper_alt + 10),
490                max: Vec2::new(center.x + 7, center.y + 2).with_z(upper_alt + 13),
491            })
492            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
493            .fill(brick.clone());
494        painter
495            .aabb(Aabb {
496                min: Vec2::new(center.x + 5, center.y - 1).with_z(upper_alt + 10),
497                max: Vec2::new(center.x + 7, center.y + 1).with_z(upper_alt + 15),
498            })
499            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
500            .fill(brick.clone());
501        painter
502            .aabb(Aabb {
503                min: Vec2::new(center.x + 5, center.y - 1).with_z(upper_alt + 10),
504                max: Vec2::new(center.x + 7, center.y + 1).with_z(upper_alt + 13),
505            })
506            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
507            .clear();
508        //door sprites
509
510        let door_rot = if self.rotation == 0.0 {
511            (2, 6)
512        } else if self.rotation == PI / 2.0 {
513            (4, 0)
514        } else if self.rotation == PI {
515            (6, 2) //good
516        } else {
517            (0, 4)
518        };
519        let sprite_fill = Fill::Block(Block::air(SpriteKind::Door).with_ori(door_rot.0).unwrap());
520        painter
521            .aabb(Aabb {
522                min: Vec3::new(center.x + 6, center.y - 1, upper_alt + 10),
523                max: Vec3::new(center.x + 7, center.y + 0, upper_alt + 11),
524            })
525            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
526            .fill(sprite_fill.clone());
527        let sprite_fill = Fill::Block(Block::air(SpriteKind::Door).with_ori(door_rot.1).unwrap());
528        painter
529            .aabb(Aabb {
530                min: Vec3::new(center.x + 6, center.y + 0, upper_alt + 10),
531                max: Vec3::new(center.x + 7, center.y + 1, upper_alt + 11),
532            })
533            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
534            .fill(sprite_fill.clone());
535
536        //bracing diagonal bits
537        painter
538            .line(
539                Vec2::new(center.x + 5, center.y - 3).with_z(upper_alt - 3),
540                Vec2::new(center.x + 17, center.y - 3).with_z(upper_alt + 8),
541                1.0,
542            )
543            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
544            .fill(wood.clone());
545        painter
546            .line(
547                Vec2::new(center.x + 5, center.y + 2).with_z(upper_alt - 3),
548                Vec2::new(center.x + 17, center.y + 2).with_z(upper_alt + 8),
549                1.0,
550            )
551            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
552            .fill(wood.clone());
553        //
554        painter
555            .line(
556                Vec2::new(center.x - 18, center.y - 3).with_z(upper_alt + 8),
557                Vec2::new(center.x - 6, center.y - 3).with_z(upper_alt - 3),
558                1.0,
559            )
560            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
561            .fill(wood.clone());
562        painter
563            .line(
564                Vec2::new(center.x - 18, center.y + 2).with_z(upper_alt + 8),
565                Vec2::new(center.x - 6, center.y + 2).with_z(upper_alt - 3),
566                1.0,
567            )
568            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
569            .fill(wood.clone());
570        //
571        painter
572            .line(
573                Vec2::new(center.x - 3, center.y - 18).with_z(upper_alt + 8),
574                Vec2::new(center.x - 3, center.y - 6).with_z(upper_alt - 3),
575                1.0,
576            )
577            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
578            .fill(wood.clone());
579        painter
580            .line(
581                Vec2::new(center.x + 2, center.y - 18).with_z(upper_alt + 8),
582                Vec2::new(center.x + 2, center.y - 6).with_z(upper_alt - 3),
583                1.0,
584            )
585            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
586            .fill(wood.clone());
587        //
588        painter
589            .line(
590                Vec2::new(center.x - 3, center.y + 5).with_z(upper_alt - 3),
591                Vec2::new(center.x - 3, center.y + 17).with_z(upper_alt + 8),
592                1.0,
593            )
594            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
595            .fill(wood.clone());
596        painter
597            .line(
598                Vec2::new(center.x + 2, center.y + 5).with_z(upper_alt - 3),
599                Vec2::new(center.x + 2, center.y + 17).with_z(upper_alt + 8),
600                1.0,
601            )
602            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
603            .fill(wood.clone());
604
605        //stairs
606        painter
607            .cylinder_with_radius(center.with_z(upper_alt + 8), 5.0, 1.0)
608            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
609            .clear();
610
611        let stairs_clear1 = painter.cylinder_with_radius(center.with_z(base), 5.0, 38.0);
612
613        painter
614            .prim(Primitive::sampling(
615                stairs_clear1,
616                spiral_staircase(center.with_z(base + 3), 6.0, 0.5, 9.0),
617            ))
618            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
619            .fill(wood.clone());
620
621        //clean up interface at top
622        painter
623            .aabb(Aabb {
624                min: Vec2::new(center.x + 1, center.y + 3).with_z(upper_alt + 8),
625                max: Vec2::new(center.x + 4, center.y + 5).with_z(upper_alt + 9),
626            })
627            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
628            .fill(wood.clone());
629        painter
630            .aabb(Aabb {
631                min: Vec2::new(center.x + 0, center.y + 2).with_z(upper_alt + 9),
632                max: Vec2::new(center.x + 6, center.y + 7).with_z(upper_alt + 10),
633            })
634            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
635            .fill(brick.clone());
636        painter
637            .aabb(Aabb {
638                min: Vec2::new(center.x + 1, center.y + 3).with_z(upper_alt + 9),
639                max: Vec2::new(center.x + 6, center.y + 7).with_z(upper_alt + 10),
640            })
641            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
642            .clear();
643        painter
644            .aabb(Aabb {
645                min: Vec2::new(center.x + 0, center.y + 2).with_z(upper_alt + 9),
646                max: Vec2::new(center.x + 1, center.y + 3).with_z(upper_alt + 17),
647            })
648            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
649            .fill(brick.clone());
650
651        let window_rot = if self.rotation == 0.0 || self.rotation == PI {
652            (2, 4)
653        } else {
654            (4, 2)
655        };
656        let sprite_fill = Fill::Block(
657            Block::air(SpriteKind::Window1)
658                .with_ori(window_rot.0)
659                .unwrap(),
660        );
661        //upper window
662        painter
663            .aabb(Aabb {
664                min: Vec2::new(center.x - 6, center.y - 1).with_z(upper_alt + 12),
665                max: Vec2::new(center.x - 5, center.y + 1).with_z(upper_alt + 15),
666            })
667            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
668            .fill(sprite_fill.clone());
669
670        //lower windows
671        painter
672            .aabb(Aabb {
673                min: Vec2::new(center.x - 6, center.y - 1).with_z(base + 19),
674                max: Vec2::new(center.x - 5, center.y + 1).with_z(base + 22),
675            })
676            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
677            .fill(sprite_fill.clone());
678        painter
679            .aabb(Aabb {
680                min: Vec2::new(center.x - 6, center.y - 1).with_z(base + 1),
681                max: Vec2::new(center.x - 5, center.y + 1).with_z(base + 4),
682            })
683            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
684            .fill(sprite_fill.clone());
685        painter
686            .aabb(Aabb {
687                min: Vec2::new(center.x + 5, center.y - 1).with_z(base + 4),
688                max: Vec2::new(center.x + 6, center.y + 1).with_z(base + 7),
689            })
690            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
691            .fill(sprite_fill.clone());
692        painter
693            .aabb(Aabb {
694                min: Vec2::new(center.x + 5, center.y - 1).with_z(base + 22),
695                max: Vec2::new(center.x + 6, center.y + 1).with_z(base + 25),
696            })
697            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
698            .fill(sprite_fill.clone());
699        painter
700            .aabb(Aabb {
701                min: Vec2::new(center.x + 5, center.y - 1).with_z(base + 30),
702                max: Vec2::new(center.x + 6, center.y + 1).with_z(base + 33),
703            })
704            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
705            .fill(sprite_fill.clone());
706
707        let sprite_fill = Fill::Block(
708            Block::air(SpriteKind::Window1)
709                .with_ori(window_rot.1)
710                .unwrap(),
711        );
712        //side windows
713        painter
714            .aabb(Aabb {
715                min: Vec2::new(center.x - 1, center.y + 5).with_z(base + 17),
716                max: Vec2::new(center.x + 1, center.y + 6).with_z(base + 20),
717            })
718            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
719            .fill(sprite_fill.clone());
720        painter
721            .aabb(Aabb {
722                min: Vec2::new(center.x - 1, center.y - 6).with_z(base + 13),
723                max: Vec2::new(center.x + 1, center.y - 5).with_z(base + 16),
724            })
725            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
726            .fill(sprite_fill.clone());
727
728        //lights
729        painter.rotated_sprite(
730            Vec2::new(center.x - 3, center.y + 5).with_z(base + 8),
731            SpriteKind::WallLampSmall,
732            4,
733        );
734        painter.rotated_sprite(
735            Vec2::new(center.x + 2, center.y + 5).with_z(base + 8),
736            SpriteKind::WallLampSmall,
737            4,
738        );
739        painter.rotated_sprite(
740            Vec2::new(center.x - 3, center.y + 5).with_z(base + 18),
741            SpriteKind::WallLampSmall,
742            4,
743        );
744        painter.rotated_sprite(
745            Vec2::new(center.x + 2, center.y + 5).with_z(base + 18),
746            SpriteKind::WallLampSmall,
747            4,
748        );
749        painter.rotated_sprite(
750            Vec2::new(center.x - 3, center.y - 6).with_z(base + 8),
751            SpriteKind::WallLampSmall,
752            0,
753        );
754        painter.rotated_sprite(
755            Vec2::new(center.x + 2, center.y - 6).with_z(base + 8),
756            SpriteKind::WallLampSmall,
757            0,
758        );
759        painter.rotated_sprite(
760            Vec2::new(center.x - 3, center.y - 6).with_z(base + 18),
761            SpriteKind::WallLampSmall,
762            0,
763        );
764        painter.rotated_sprite(
765            Vec2::new(center.x + 2, center.y - 6).with_z(base + 18),
766            SpriteKind::WallLampSmall,
767            0,
768        );
769
770        painter.rotated_sprite(
771            Vec2::new(center.x + 5, center.y - 3).with_z(base + 13),
772            SpriteKind::WallLampSmall,
773            2,
774        );
775        painter.rotated_sprite(
776            Vec2::new(center.x + 5, center.y + 2).with_z(base + 13),
777            SpriteKind::WallLampSmall,
778            2,
779        );
780        painter.rotated_sprite(
781            Vec2::new(center.x + 5, center.y - 3).with_z(base + 29),
782            SpriteKind::WallLampSmall,
783            2,
784        );
785        painter.rotated_sprite(
786            Vec2::new(center.x + 5, center.y + 2).with_z(base + 29),
787            SpriteKind::WallLampSmall,
788            2,
789        );
790        painter.rotated_sprite(
791            Vec2::new(center.x - 6, center.y - 3).with_z(base + 13),
792            SpriteKind::WallLampSmall,
793            6,
794        );
795        painter.rotated_sprite(
796            Vec2::new(center.x - 6, center.y + 2).with_z(base + 13),
797            SpriteKind::WallLampSmall,
798            6,
799        );
800        painter.rotated_sprite(
801            Vec2::new(center.x - 6, center.y - 3).with_z(base + 29),
802            SpriteKind::WallLampSmall,
803            6,
804        );
805        painter.rotated_sprite(
806            Vec2::new(center.x - 6, center.y + 2).with_z(base + 29),
807            SpriteKind::WallLampSmall,
808            6,
809        );
810        //upper lighting
811        for dir in DIAGONALS {
812            let pos = (center + dir * 12).with_z(upper_alt + 7);
813            painter.sprite(pos, SpriteKind::Lantern)
814        }
815        for dir in CARDINALS {
816            let pos = (center + dir * 24).with_z(upper_alt + 7);
817            painter.sprite(pos, SpriteKind::Lantern)
818        }
819        let sprite_fill = Fill::Block(Block::air(SpriteKind::Lantern).with_ori(2).unwrap());
820        //on cone lamps
821        painter
822            .aabb(Aabb {
823                min: Vec3::new(center.x - 6, center.y + 5, upper_alt + 16),
824                max: Vec3::new(center.x - 5, center.y + 6, upper_alt + 17),
825            })
826            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
827            .fill(sprite_fill.clone());
828        painter
829            .aabb(Aabb {
830                min: Vec3::new(center.x + 5, center.y + 5, upper_alt + 16),
831                max: Vec3::new(center.x + 6, center.y + 6, upper_alt + 17),
832            })
833            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
834            .fill(sprite_fill.clone());
835        painter
836            .aabb(Aabb {
837                min: Vec3::new(center.x - 6, center.y - 6, upper_alt + 16),
838                max: Vec3::new(center.x - 5, center.y - 5, upper_alt + 17),
839            })
840            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
841            .fill(sprite_fill.clone());
842
843        // laterns on walls of agent desk
844        let agent_lamp1_ori = (((self.rotation / std::f32::consts::FRAC_PI_2) as u8 * 2) + 6) % 8;
845        painter
846            .aabb(Aabb {
847                min: Vec3::new(center.x + 7, center.y - 3, upper_alt + 13),
848                max: Vec3::new(center.x + 8, center.y - 4, upper_alt + 14),
849            })
850            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
851            .fill(Fill::Block(
852                Block::air(SpriteKind::LanternAirshipWallBrownS)
853                    .with_ori(agent_lamp1_ori)
854                    .unwrap(),
855            ));
856        let agent_lamp2_ori = (self.rotation / std::f32::consts::FRAC_PI_2) as u8 * 2;
857        painter
858            .aabb(Aabb {
859                min: Vec3::new(center.x + 3, center.y - 7, upper_alt + 13),
860                max: Vec3::new(center.x + 4, center.y - 8, upper_alt + 14),
861            })
862            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
863            .fill(Fill::Block(
864                Block::air(SpriteKind::LanternAirshipWallBrownS)
865                    .with_ori(agent_lamp2_ori)
866                    .unwrap(),
867            ));
868
869        //interior
870
871        painter
872            .aabb(Aabb {
873                min: Vec3::new(center.x - 2, center.y - 3, base + 6),
874                max: Vec3::new(center.x - 1, center.y + -2, base + 7),
875            })
876            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
877            .fill(sprite_fill.clone());
878        painter
879            .aabb(Aabb {
880                min: Vec3::new(center.x - 2, center.y - 3, base + 15),
881                max: Vec3::new(center.x - 1, center.y + -2, base + 16),
882            })
883            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
884            .fill(sprite_fill.clone());
885        painter
886            .aabb(Aabb {
887                min: Vec3::new(center.x - 2, center.y - 3, base + 24),
888                max: Vec3::new(center.x - 1, center.y + -2, base + 25),
889            })
890            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
891            .fill(sprite_fill.clone());
892        painter
893            .aabb(Aabb {
894                min: Vec3::new(center.x - 2, center.y - 3, base + 33),
895                max: Vec3::new(center.x - 1, center.y + -2, base + 34),
896            })
897            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
898            .fill(sprite_fill.clone());
899        painter
900            .aabb(Aabb {
901                min: Vec3::new(center.x - 2, center.y - 3, base + 44),
902                max: Vec3::new(center.x - 1, center.y + -2, base + 45),
903            })
904            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
905            .fill(sprite_fill.clone());
906
907        // crate and barrel sprites
908        let mut sprite_positions = vec![];
909        for a in 0..5 {
910            sprite_positions.push(Vec2::new(center.x + 1 + a, center.y + 2));
911        }
912        for b in 0..=1 {
913            sprite_positions.push(Vec2::new(center.x, center.y + 3 + b));
914        }
915        for sprite_pos in sprite_positions {
916            let rows = (RandomField::new(0).get(sprite_pos.with_z(base)) % 3) as i32;
917            for r in 0..rows {
918                painter
919                    .aabb(Aabb {
920                        min: sprite_pos.with_z(upper_alt + 10 + r),
921                        max: (sprite_pos + 1).with_z(upper_alt + 11 + r),
922                    })
923                    .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
924                    .fill(Fill::Block(Block::air(
925                        match (RandomField::new(0).get(sprite_pos.with_z(base + r)) % 2) as i32 {
926                            0 => SpriteKind::Barrel,
927                            _ => SpriteKind::CrateBlock,
928                        },
929                    )));
930            }
931        }
932
933        // campfire
934        painter.spawn(
935            EntityInfo::at(self.campfire_pos.map(|e| e as f32 + 0.5))
936                .into_special(SpecialEntity::Waypoint),
937        );
938
939        // Rampart
940        painter
941            .cylinder_with_radius(center.with_z(base - 1), foundation_radius - 1.0, 1.0)
942            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
943            .fill(grass.clone());
944        painter
945            .cylinder_with_radius(center.with_z(base - 1), 10.0, 1.0)
946            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
947            .fill(brick.clone());
948
949        painter
950            .cylinder_with_radius(
951                center.with_z(min_foundation_alt),
952                foundation_radius,
953                (base - min_foundation_alt - 1) as f32,
954            )
955            .rotate_about(Mat3::rotation_z(self.rotation).as_(), center.with_z(base))
956            .fill(dirt.clone());
957
958        let stair_height = 8;
959        let stair_width = 3;
960        let stair_levels = (base - min_foundation_alt - 1) / (stair_height + 1);
961        let stair_drop = 4;
962        let stair_landing_space = stair_height + 3;
963        let stairtop = base - 1;
964        /*
965           Rampart stair components
966
967           ┌────┐
968           │ SL │
969           ├────┼┐
970           │    │ ┐
971           │ SC │  ┐
972           │    │   ┐
973           │    │ S  ┐
974           │    ├─────┬────┐
975           │    │  MD │ SL │
976           │    │     ├────┤
977           └────┼─────┤    │
978                └  S  │ SC │
979                 └    │    │
980                  └   │    │
981                   └  │    │
982                    └ └────┘
983           S - Stair
984           MD - Middle Dirt
985           SL - Stair Landing
986           SC - Stair Column
987
988           The diagram shows a stair section for an even-numbered level;
989           odd-numbered levels are mirrored left-to-right.
990           The staircase has a cap made of brick and the stair sides are dirt.
991           The stair cap is not shown in the diagram.
992           The above components are rendered for each level of the stairs,
993           and flipped back and forth for as many levels as will fit in the
994           rampart height.
995           These components are reused, rotated, and translated to build the full staircase.
996        */
997        let edge_clear = aabb(
998            Vec2::new(center.x - 4, center.y + foundation_radius as i32 - 1)
999                .with_z(min_foundation_alt),
1000            Vec2::new(center.x + 4, center.y + foundation_radius as i32).with_z(base + 1),
1001        );
1002        let edge_fill = aabb(
1003            Vec2::new(center.x - 7, center.y + foundation_radius as i32 - 2).with_z(stairtop),
1004            Vec2::new(center.x + 6, center.y + foundation_radius as i32 - 2).with_z(stairtop),
1005        );
1006        let stair = aabb(
1007            Vec2::new(center.x - 4, center.y + foundation_radius as i32 - 1)
1008                .with_z(stairtop - stair_height),
1009            Vec2::new(
1010                center.x + 3,
1011                center.y + foundation_radius as i32 + stair_width - 2,
1012            )
1013            .with_z(stairtop - 1),
1014        );
1015        let middirt = aabb(
1016            Vec2::new(center.x - 4, center.y + foundation_radius as i32 - 1)
1017                .with_z(stairtop - stair_height - 1),
1018            Vec2::new(
1019                center.x + 3,
1020                center.y + foundation_radius as i32 + stair_width - 2,
1021            )
1022            .with_z(stairtop - stair_height - (stair_drop + 1)),
1023        );
1024        let stair_landing = aabb(
1025            Vec2::new(center.x + 6, center.y + foundation_radius as i32 - 1).with_z(stairtop),
1026            Vec2::new(
1027                center.x + 4,
1028                center.y + foundation_radius as i32 + stair_width - 2,
1029            )
1030            .with_z(stairtop),
1031        );
1032        let stair_column = aabb(
1033            Vec2::new(center.x + 6, center.y + foundation_radius as i32 - 1).with_z(stairtop - 1),
1034            Vec2::new(
1035                center.x + 4,
1036                center.y + foundation_radius as i32 + stair_width - 2,
1037            )
1038            .with_z(stairtop - (stair_height + stair_drop)),
1039        );
1040
1041        let slice = painter.aabb(edge_clear);
1042        let edge = painter.aabb(edge_fill);
1043        let stair_cap_even =
1044            painter
1045                .ramp(stair, Dir2::X)
1046                .intersect(painter.ramp(stair, Dir2::X).rotate_about(
1047                    Mat3::new(-1, 0, 0, 0, 1, 0, 0, 0, -1),
1048                    center.with_z(stairtop - 4),
1049                ));
1050        let stair_cap_odd =
1051            stair_cap_even.rotate_about(Mat3::new(-1, 0, 0, 0, -1, 0, 0, 0, 1), stair.center());
1052        let stair_base_even = painter
1053            .ramp(stair, Dir2::X)
1054            .intersect(painter.ramp(stair, Dir2::X).translate(Vec3::new(1, 0, 0)))
1055            .union(painter.aabb(middirt))
1056            .union(
1057                painter
1058                    .ramp(stair, Dir2::X)
1059                    .rotate_about(
1060                        Mat3::new(-1, 0, 0, 0, 1, 0, 0, 0, -1),
1061                        center.with_z(stairtop - 4),
1062                    )
1063                    .translate(Vec3::new(0, 0, -(stair_height + stair_drop))),
1064            );
1065        let stair_base_odd =
1066            stair_base_even.rotate_about(Mat3::new(-1, 0, 0, 0, -1, 0, 0, 0, 1), stair.center());
1067        let stair_column_cap = painter.aabb(stair_landing);
1068        let stair_column_base = painter.aabb(stair_column);
1069
1070        // for each side of the rampart
1071        for side in 0..4 {
1072            let rot: f32 = (self.rotation + side as f32 * std::f32::consts::FRAC_PI_2)
1073                .rem_euclid(std::f32::consts::TAU);
1074
1075            // Flatten the rampart cylinder where the stairs will go
1076            painter.fill(
1077                slice.rotate_about(Mat3::rotation_z(rot).as_(), center.with_z(base)),
1078                Fill::Block(Block::empty()),
1079            );
1080            painter.fill(
1081                edge.rotate_about(Mat3::rotation_z(rot).as_(), center.with_z(base)),
1082                brick.clone(),
1083            );
1084            painter.fill(
1085                edge.translate(Vec3::new(0, 0, 1))
1086                    .rotate_about(Mat3::rotation_z(rot).as_(), center.with_z(base)),
1087                Fill::Block(Block::empty()),
1088            );
1089            // The edge near the stairs is filled in with bricks and gaps at the edge of the
1090            // stair wall that was removed.
1091            let picketx = 3;
1092            for i in 0..3 {
1093                painter
1094                    .line(
1095                        Vec2::new(
1096                            center.x + picketx - i * 4,
1097                            center.y + foundation_radius as i32 - 2,
1098                        )
1099                        .with_z(base),
1100                        Vec2::new(
1101                            center.x + picketx - i * 4 - 2,
1102                            center.y + foundation_radius as i32 - 2,
1103                        )
1104                        .with_z(base),
1105                        0.5,
1106                    )
1107                    .rotate_about(Mat3::rotation_z(rot).as_(), center.with_z(base))
1108                    .fill(brick.clone());
1109            }
1110            painter
1111                .aabb(aabb(
1112                    Vec2::new(center.x + 4, center.y + 4).with_z(base - 1),
1113                    Vec2::new(center.x + 6, center.y + foundation_radius as i32 - 2)
1114                        .with_z(base - 1),
1115                ))
1116                .rotate_about(Mat3::rotation_z(rot).as_(), center.with_z(base))
1117                .fill(brick.clone());
1118
1119            // if no stairs, just fill in around the rampart top with dirt
1120            if stair_levels == 0 {
1121                painter
1122                    .aabb(aabb(
1123                        Vec2::new(center.x - 4, center.y + foundation_radius as i32 - 1)
1124                            .with_z(base - 7),
1125                        Vec2::new(center.x + 4, center.y + foundation_radius as i32)
1126                            .with_z(base - 2),
1127                    ))
1128                    .rotate_about(Mat3::rotation_z(rot).as_(), center.with_z(base))
1129                    .fill(dirt.clone());
1130                continue;
1131            }
1132
1133            // for each level of stairs
1134            for level in 0..stair_levels {
1135                let y_off = level % 2 * stair_width;
1136                let stair_y_off = if level % 2 > 0 && stair_width % 2 > 0 {
1137                    y_off + 1
1138                } else {
1139                    y_off
1140                };
1141                if level % 2 == 0 {
1142                    painter.fill(
1143                        stair_cap_even
1144                            .translate(Vec3::new(0, stair_y_off, level * -(stair_height + 1)))
1145                            .rotate_about(Mat3::rotation_z(rot).as_(), center.with_z(base)),
1146                        brick.clone(),
1147                    );
1148                    painter.fill(
1149                        stair_base_even
1150                            .translate(Vec3::new(0, stair_y_off, level * -(stair_height + 1)))
1151                            .rotate_about(Mat3::rotation_z(rot).as_(), center.with_z(base)),
1152                        dirt.clone(),
1153                    );
1154                } else {
1155                    painter.fill(
1156                        stair_cap_odd
1157                            .translate(Vec3::new(0, stair_y_off, level * -(stair_height + 1)))
1158                            .rotate_about(Mat3::rotation_z(rot).as_(), center.with_z(base)),
1159                        brick.clone(),
1160                    );
1161                    painter.fill(
1162                        stair_base_odd
1163                            .translate(Vec3::new(0, stair_y_off, level * -(stair_height + 1)))
1164                            .rotate_about(Mat3::rotation_z(rot).as_(), center.with_z(base)),
1165                        dirt.clone(),
1166                    );
1167                }
1168                painter.fill(
1169                    stair_column_cap
1170                        .translate(Vec3::new(
1171                            level % 2 * -stair_landing_space,
1172                            y_off,
1173                            level * -(stair_height + 1),
1174                        ))
1175                        .rotate_about(Mat3::rotation_z(rot).as_(), center.with_z(base)),
1176                    brick.clone(),
1177                );
1178                painter.fill(
1179                    stair_column_base
1180                        .translate(Vec3::new(
1181                            level % 2 * -stair_landing_space,
1182                            y_off,
1183                            level * -(stair_height + 1),
1184                        ))
1185                        .rotate_about(Mat3::rotation_z(rot).as_(), center.with_z(base)),
1186                    dirt.clone(),
1187                );
1188                painter.fill(
1189                    stair_column_cap
1190                        .translate(Vec3::new(
1191                            (level + 1) % 2 * -stair_landing_space,
1192                            y_off,
1193                            (level + 1) * -(stair_height + 1),
1194                        ))
1195                        .rotate_about(Mat3::rotation_z(rot).as_(), center.with_z(base)),
1196                    brick.clone(),
1197                );
1198                painter.fill(
1199                    stair_column_base
1200                        .translate(Vec3::new(
1201                            (level + 1) % 2 * -stair_landing_space,
1202                            y_off,
1203                            (level + 1) * -(stair_height + 1),
1204                        ))
1205                        .rotate_about(Mat3::rotation_z(rot).as_(), center.with_z(base)),
1206                    dirt.clone(),
1207                );
1208                let lamp_pos = Vec2::new(
1209                    center.x - 5 + level % 2 * (stair_height + 1),
1210                    center.y + foundation_radius as i32 - 1,
1211                )
1212                .with_z(base - 5 - level * (stair_height + 1));
1213                let lamp_ori = (((rot / std::f32::consts::FRAC_PI_2) as u8 * 2) + 10) % 8;
1214                painter
1215                    .aabb(aabb(lamp_pos, lamp_pos))
1216                    .rotate_about(Mat3::rotation_z(rot).as_(), center.with_z(base))
1217                    .fill(Fill::sprite_ori(
1218                        SpriteKind::LanternAirshipWallBrownS,
1219                        lamp_ori,
1220                    ));
1221            }
1222        }
1223    }
1224}
1225
1226fn aabb(min: Vec3<i32>, max: Vec3<i32>) -> Aabb<i32> {
1227    let aabb = Aabb { min, max }.made_valid();
1228    Aabb {
1229        min: aabb.min,
1230        max: aabb.max + 1,
1231    }
1232}