1use crate::prelude::*;
2use rusterix::PixelSource;
3use std::str::FromStr;
4
5pub const CREATE_STAIRS_ACTION_ID: &str = "4f4d41d0-7f67-4c1f-a8d2-f0ab4a0be6a1";
6
7pub struct CreateStairs {
8 id: TheId,
9 nodeui: TheNodeUI,
10}
11
12impl CreateStairs {
13 fn parse_tile_source(text: &str) -> Option<Value> {
14 let id = Uuid::parse_str(text.trim()).ok()?;
15 Some(Value::Source(PixelSource::TileId(id)))
16 }
17
18 fn apply_sector_stairs(&self, map: &mut Map, sector_id: u32) -> bool {
19 let direction = self
20 .nodeui
21 .get_i32_value("actionStairsDirection")
22 .unwrap_or(0)
23 .clamp(0, 3);
24 let steps = self
25 .nodeui
26 .get_i32_value("actionStairsSteps")
27 .unwrap_or(6)
28 .max(1);
29 let total_height = self
30 .nodeui
31 .get_f32_value("actionStairsTotalHeight")
32 .unwrap_or(1.0)
33 .max(0.0);
34 let fill_sides = self
35 .nodeui
36 .get_bool_value("actionStairsFillSides")
37 .unwrap_or(true);
38
39 let tile_id_text = self
40 .nodeui
41 .get_text_value("actionStairsTileId")
42 .unwrap_or_default();
43 let tread_tile_id_text = self
44 .nodeui
45 .get_text_value("actionStairsTreadTileId")
46 .unwrap_or_default();
47 let riser_tile_id_text = self
48 .nodeui
49 .get_text_value("actionStairsRiserTileId")
50 .unwrap_or_default();
51 let side_tile_id_text = self
52 .nodeui
53 .get_text_value("actionStairsSideTileId")
54 .unwrap_or_default();
55
56 let Some(sector) = map.find_sector_mut(sector_id) else {
57 return false;
58 };
59
60 if total_height <= 0.0 {
61 sector
62 .properties
63 .set("sector_feature", Value::Str("None".to_string()));
64 return true;
65 }
66
67 sector
68 .properties
69 .set("sector_feature", Value::Str("Stairs".to_string()));
70 sector
71 .properties
72 .set("stairs_direction", Value::Int(direction));
73 sector.properties.set("stairs_steps", Value::Int(steps));
74 sector
75 .properties
76 .set("stairs_total_height", Value::Float(total_height));
77 sector
78 .properties
79 .set("stairs_fill_sides", Value::Bool(fill_sides));
80
81 if let Some(src) = Self::parse_tile_source(&tile_id_text) {
82 sector.properties.set("stairs_tile_source", src);
83 } else {
84 sector.properties.remove("stairs_tile_source");
85 }
86 if let Some(src) = Self::parse_tile_source(&tread_tile_id_text) {
87 sector.properties.set("stairs_tread_source", src);
88 } else {
89 sector.properties.remove("stairs_tread_source");
90 }
91 if let Some(src) = Self::parse_tile_source(&riser_tile_id_text) {
92 sector.properties.set("stairs_riser_source", src);
93 } else {
94 sector.properties.remove("stairs_riser_source");
95 }
96 if let Some(src) = Self::parse_tile_source(&side_tile_id_text) {
97 sector.properties.set("stairs_side_source", src);
98 } else {
99 sector.properties.remove("stairs_side_source");
100 }
101
102 true
103 }
104}
105
106impl Action for CreateStairs {
107 fn new() -> Self
108 where
109 Self: Sized,
110 {
111 let mut nodeui = TheNodeUI::default();
112
113 nodeui.add_item(TheNodeUIItem::OpenTree("stairs".into()));
114 nodeui.add_item(TheNodeUIItem::Selector(
115 "actionStairsDirection".into(),
116 "".into(),
117 "".into(),
118 vec!["north".into(), "east".into(), "south".into(), "west".into()],
119 0,
120 ));
121 nodeui.add_item(TheNodeUIItem::IntEditSlider(
122 "actionStairsSteps".into(),
123 "".into(),
124 "".into(),
125 6,
126 1..=64,
127 false,
128 ));
129 nodeui.add_item(TheNodeUIItem::FloatEditSlider(
130 "actionStairsTotalHeight".into(),
131 "".into(),
132 "".into(),
133 1.0,
134 0.0..=16.0,
135 false,
136 ));
137 nodeui.add_item(TheNodeUIItem::Checkbox(
138 "actionStairsFillSides".into(),
139 "".into(),
140 "".into(),
141 true,
142 ));
143 nodeui.add_item(TheNodeUIItem::CloseTree);
144
145 nodeui.add_item(TheNodeUIItem::OpenTree("material".into()));
146 nodeui.add_item(TheNodeUIItem::Text(
147 "actionStairsTileId".into(),
148 "".into(),
149 "".into(),
150 "".into(),
151 None,
152 false,
153 ));
154 nodeui.add_item(TheNodeUIItem::Text(
155 "actionStairsTreadTileId".into(),
156 "".into(),
157 "".into(),
158 "".into(),
159 None,
160 false,
161 ));
162 nodeui.add_item(TheNodeUIItem::Text(
163 "actionStairsRiserTileId".into(),
164 "".into(),
165 "".into(),
166 "".into(),
167 None,
168 false,
169 ));
170 nodeui.add_item(TheNodeUIItem::Text(
171 "actionStairsSideTileId".into(),
172 "".into(),
173 "".into(),
174 "".into(),
175 None,
176 false,
177 ));
178 nodeui.add_item(TheNodeUIItem::CloseTree);
179
180 nodeui.add_item(TheNodeUIItem::Markdown("desc".into(), "".into()));
181
182 Self {
183 id: TheId::named_with_id(
184 "Create Stairs",
185 Uuid::from_str(CREATE_STAIRS_ACTION_ID).unwrap(),
186 ),
187 nodeui,
188 }
189 }
190
191 fn id(&self) -> TheId {
192 self.id.clone()
193 }
194
195 fn info(&self) -> String {
196 "Configure non-destructive stairs on selected sectors.".to_string()
197 }
198
199 fn role(&self) -> ActionRole {
200 ActionRole::Editor
201 }
202
203 fn is_applicable(&self, map: &Map, _ctx: &mut TheContext, server_ctx: &ServerContext) -> bool {
204 if server_ctx.editor_view_mode == EditorViewMode::D2 {
205 return false;
206 }
207 !map.selected_sectors.is_empty()
208 }
209
210 fn load_params(&mut self, map: &Map) {
211 let Some(sector_id) = map.selected_sectors.first() else {
212 return;
213 };
214 let Some(sector) = map.find_sector(*sector_id) else {
215 return;
216 };
217
218 self.nodeui.set_i32_value(
219 "actionStairsDirection",
220 sector.properties.get_int_default("stairs_direction", 0),
221 );
222 self.nodeui.set_i32_value(
223 "actionStairsSteps",
224 sector.properties.get_int_default("stairs_steps", 6),
225 );
226 self.nodeui.set_f32_value(
227 "actionStairsTotalHeight",
228 sector
229 .properties
230 .get_float_default("stairs_total_height", 1.0),
231 );
232 self.nodeui.set_bool_value(
233 "actionStairsFillSides",
234 sector
235 .properties
236 .get_bool_default("stairs_fill_sides", true),
237 );
238
239 let tile_id_text = match sector.properties.get("stairs_tile_source") {
240 Some(Value::Source(PixelSource::TileId(id))) => id.to_string(),
241 _ => String::new(),
242 };
243 let tread_tile_id_text = match sector.properties.get("stairs_tread_source") {
244 Some(Value::Source(PixelSource::TileId(id))) => id.to_string(),
245 _ => String::new(),
246 };
247 let riser_tile_id_text = match sector.properties.get("stairs_riser_source") {
248 Some(Value::Source(PixelSource::TileId(id))) => id.to_string(),
249 _ => String::new(),
250 };
251 let side_tile_id_text = match sector.properties.get("stairs_side_source") {
252 Some(Value::Source(PixelSource::TileId(id))) => id.to_string(),
253 _ => String::new(),
254 };
255
256 self.nodeui
257 .set_text_value("actionStairsTileId", tile_id_text);
258 self.nodeui
259 .set_text_value("actionStairsTreadTileId", tread_tile_id_text);
260 self.nodeui
261 .set_text_value("actionStairsRiserTileId", riser_tile_id_text);
262 self.nodeui
263 .set_text_value("actionStairsSideTileId", side_tile_id_text);
264 }
265
266 fn apply(
267 &self,
268 map: &mut Map,
269 _ui: &mut TheUI,
270 _ctx: &mut TheContext,
271 server_ctx: &mut ServerContext,
272 ) -> Option<ProjectUndoAtom> {
273 let prev = map.clone();
274 let mut changed = false;
275
276 for sector_id in map.selected_sectors.clone() {
277 changed |= self.apply_sector_stairs(map, sector_id);
278 }
279
280 if changed {
281 Some(ProjectUndoAtom::MapEdit(
282 server_ctx.pc,
283 Box::new(prev),
284 Box::new(map.clone()),
285 ))
286 } else {
287 None
288 }
289 }
290
291 fn params(&self) -> TheNodeUI {
292 self.nodeui.clone()
293 }
294
295 fn handle_event(
296 &mut self,
297 event: &TheEvent,
298 _project: &mut Project,
299 _ui: &mut TheUI,
300 _ctx: &mut TheContext,
301 _server_ctx: &mut ServerContext,
302 ) -> bool {
303 self.nodeui.handle_event(event)
304 }
305}