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