1use crate::prelude::*;
2use rusterix::PixelSource;
3use std::str::FromStr;
4
5pub const CREATE_PALISADE_ACTION_ID: &str = "f6f4df4c-2cde-4ab5-98ff-2f7f4f62383e";
6
7pub struct CreatePalisade {
8 id: TheId,
9 nodeui: TheNodeUI,
10}
11
12impl CreatePalisade {
13 fn apply_linedef_feature(&self, map: &mut Map, linedef_id: u32) -> bool {
14 let spacing = self
15 .nodeui
16 .get_f32_value("actionLayoutSpacing")
17 .unwrap_or(1.0)
18 .max(0.1);
19 let segment_size = self
20 .nodeui
21 .get_f32_value("actionLayoutSegmentSize")
22 .unwrap_or(0.75)
23 .max(0.05);
24 let shape = self
25 .nodeui
26 .get_i32_value("actionShapeStakeShape")
27 .unwrap_or(1);
28 let depth = self
29 .nodeui
30 .get_f32_value("actionShapeDepth")
31 .unwrap_or(0.12)
32 .max(0.02);
33 let round_segments = self
34 .nodeui
35 .get_i32_value("actionShapeRoundSegments")
36 .unwrap_or(8)
37 .max(3);
38 let height = self.nodeui.get_f32_value("actionHeightBase").unwrap_or(2.0);
39 let top_mode = self.nodeui.get_i32_value("actionTopMode").unwrap_or(0);
40 let top_height = self
41 .nodeui
42 .get_f32_value("actionTopHeight")
43 .unwrap_or(0.5)
44 .max(0.0);
45 let height_variation = self
46 .nodeui
47 .get_f32_value("actionHeightVariation")
48 .unwrap_or(0.35)
49 .max(0.0);
50 let lean_amount = self
51 .nodeui
52 .get_f32_value("actionLeanAmount")
53 .unwrap_or(0.0)
54 .max(0.0);
55 let lean_randomness = self
56 .nodeui
57 .get_f32_value("actionLeanRandomness")
58 .unwrap_or(1.0)
59 .clamp(0.0, 1.0);
60 let tile_id_text = self
61 .nodeui
62 .get_text_value("actionMaterialTileId")
63 .unwrap_or_default();
64 let tile_id = Uuid::parse_str(tile_id_text.trim()).ok();
65
66 let Some(linedef) = map.find_linedef_mut(linedef_id) else {
67 return false;
68 };
69
70 if height <= 0.0 {
72 linedef
73 .properties
74 .set("linedef_feature", Value::Str("None".to_string()));
75 return true;
76 }
77
78 linedef
79 .properties
80 .set("linedef_feature", Value::Str("Palisade".to_string()));
81 linedef
82 .properties
83 .set("feature_layout_spacing", Value::Float(spacing));
84 linedef
85 .properties
86 .set("feature_segment_size", Value::Float(segment_size));
87 linedef.properties.set("feature_shape", Value::Int(shape));
88 linedef.properties.set("feature_depth", Value::Float(depth));
89 linedef
90 .properties
91 .set("feature_round_segments", Value::Int(round_segments));
92 linedef
93 .properties
94 .set("feature_height", Value::Float(height));
95 linedef
96 .properties
97 .set("feature_top_mode", Value::Int(top_mode));
98 linedef
99 .properties
100 .set("feature_top_height", Value::Float(top_height));
101 linedef
102 .properties
103 .set("feature_height_variation", Value::Float(height_variation));
104 linedef
105 .properties
106 .set("feature_lean_amount", Value::Float(lean_amount));
107 linedef
108 .properties
109 .set("feature_lean_randomness", Value::Float(lean_randomness));
110
111 if let Some(id) = tile_id {
112 linedef
113 .properties
114 .set("feature_source", Value::Source(PixelSource::TileId(id)));
115 } else {
116 linedef.properties.remove("feature_source");
117 }
118
119 true
120 }
121}
122
123impl Action for CreatePalisade {
124 fn new() -> Self
125 where
126 Self: Sized,
127 {
128 let mut nodeui = TheNodeUI::default();
129
130 nodeui.add_item(TheNodeUIItem::OpenTree("material".into()));
131 nodeui.add_item(TheNodeUIItem::Text(
132 "actionMaterialTileId".into(),
133 "".into(),
134 "".into(),
135 "".into(),
136 None,
137 false,
138 ));
139 nodeui.add_item(TheNodeUIItem::CloseTree);
140
141 nodeui.add_item(TheNodeUIItem::OpenTree("layout".into()));
142 nodeui.add_item(TheNodeUIItem::FloatEditSlider(
143 "actionLayoutSpacing".into(),
144 "".into(),
145 "".into(),
146 1.0,
147 0.1..=8.0,
148 false,
149 ));
150 nodeui.add_item(TheNodeUIItem::FloatEditSlider(
151 "actionLayoutSegmentSize".into(),
152 "".into(),
153 "".into(),
154 0.75,
155 0.05..=8.0,
156 false,
157 ));
158 nodeui.add_item(TheNodeUIItem::CloseTree);
159
160 nodeui.add_item(TheNodeUIItem::OpenTree("shape".into()));
161 nodeui.add_item(TheNodeUIItem::Selector(
162 "actionShapeStakeShape".into(),
163 "".into(),
164 "".into(),
165 vec!["flat".into(), "square".into(), "round".into()],
166 1,
167 ));
168 nodeui.add_item(TheNodeUIItem::FloatEditSlider(
169 "actionShapeDepth".into(),
170 "".into(),
171 "".into(),
172 0.12,
173 0.02..=2.0,
174 false,
175 ));
176 nodeui.add_item(TheNodeUIItem::IntEditSlider(
177 "actionShapeRoundSegments".into(),
178 "".into(),
179 "".into(),
180 8,
181 3..=24,
182 false,
183 ));
184 nodeui.add_item(TheNodeUIItem::CloseTree);
185
186 nodeui.add_item(TheNodeUIItem::OpenTree("height".into()));
187 nodeui.add_item(TheNodeUIItem::FloatEditSlider(
188 "actionHeightBase".into(),
189 "".into(),
190 "".into(),
191 2.0,
192 0.0..=8.0,
193 false,
194 ));
195 nodeui.add_item(TheNodeUIItem::FloatEditSlider(
196 "actionHeightVariation".into(),
197 "".into(),
198 "".into(),
199 0.35,
200 0.0..=4.0,
201 false,
202 ));
203 nodeui.add_item(TheNodeUIItem::CloseTree);
204
205 nodeui.add_item(TheNodeUIItem::OpenTree("top".into()));
206 nodeui.add_item(TheNodeUIItem::Selector(
207 "actionTopMode".into(),
208 "".into(),
209 "".into(),
210 vec![
211 "flat".into(),
212 "spike".into(),
213 "bevel".into(),
214 "random".into(),
215 ],
216 0,
217 ));
218 nodeui.add_item(TheNodeUIItem::FloatEditSlider(
219 "actionTopHeight".into(),
220 "".into(),
221 "".into(),
222 0.5,
223 0.0..=4.0,
224 false,
225 ));
226 nodeui.add_item(TheNodeUIItem::CloseTree);
227
228 nodeui.add_item(TheNodeUIItem::OpenTree("lean".into()));
229 nodeui.add_item(TheNodeUIItem::FloatEditSlider(
230 "actionLeanAmount".into(),
231 "".into(),
232 "".into(),
233 0.0,
234 0.0..=1.5,
235 false,
236 ));
237 nodeui.add_item(TheNodeUIItem::FloatEditSlider(
238 "actionLeanRandomness".into(),
239 "".into(),
240 "".into(),
241 1.0,
242 0.0..=1.0,
243 false,
244 ));
245 nodeui.add_item(TheNodeUIItem::CloseTree);
246
247 nodeui.add_item(TheNodeUIItem::Markdown("desc".into(), "".into()));
248
249 Self {
250 id: TheId::named_with_id(
251 "Create Palisade",
252 Uuid::from_str(CREATE_PALISADE_ACTION_ID).unwrap(),
253 ),
254 nodeui,
255 }
256 }
257
258 fn id(&self) -> TheId {
259 self.id.clone()
260 }
261
262 fn info(&self) -> String {
263 "Configure a non-destructive palisade feature on selected linedefs.".to_string()
264 }
265
266 fn role(&self) -> ActionRole {
267 ActionRole::Editor
268 }
269
270 fn is_applicable(&self, map: &Map, _ctx: &mut TheContext, server_ctx: &ServerContext) -> bool {
271 if server_ctx.editor_view_mode == EditorViewMode::D2 {
273 return false;
274 }
275 map.selected_sectors.is_empty() && !map.selected_linedefs.is_empty()
276 }
277
278 fn load_params(&mut self, map: &Map) {
279 let Some(linedef_id) = map.selected_linedefs.first() else {
280 return;
281 };
282 let Some(linedef) = map.find_linedef(*linedef_id) else {
283 return;
284 };
285
286 self.nodeui.set_f32_value(
287 "actionLayoutSpacing",
288 linedef
289 .properties
290 .get_float_default("feature_layout_spacing", 1.0),
291 );
292 self.nodeui.set_f32_value(
293 "actionLayoutSegmentSize",
294 linedef
295 .properties
296 .get_float_default("feature_segment_size", 0.75),
297 );
298 self.nodeui.set_i32_value(
299 "actionShapeStakeShape",
300 linedef.properties.get_int_default("feature_shape", 1),
301 );
302 self.nodeui.set_f32_value(
303 "actionShapeDepth",
304 linedef.properties.get_float_default("feature_depth", 0.12),
305 );
306 self.nodeui.set_i32_value(
307 "actionShapeRoundSegments",
308 linedef
309 .properties
310 .get_int_default("feature_round_segments", 8),
311 );
312 self.nodeui.set_f32_value(
313 "actionHeightBase",
314 linedef.properties.get_float_default("feature_height", 2.0),
315 );
316 self.nodeui.set_f32_value(
317 "actionHeightVariation",
318 linedef
319 .properties
320 .get_float_default("feature_height_variation", 0.35),
321 );
322 self.nodeui.set_i32_value(
323 "actionTopMode",
324 linedef.properties.get_int_default("feature_top_mode", 0),
325 );
326 self.nodeui.set_f32_value(
327 "actionTopHeight",
328 linedef
329 .properties
330 .get_float_default("feature_top_height", 0.5),
331 );
332 self.nodeui.set_f32_value(
333 "actionLeanAmount",
334 linedef
335 .properties
336 .get_float_default("feature_lean_amount", 0.0),
337 );
338 self.nodeui.set_f32_value(
339 "actionLeanRandomness",
340 linedef
341 .properties
342 .get_float_default("feature_lean_randomness", 1.0),
343 );
344
345 let tile_id_text = match linedef.properties.get("feature_source") {
346 Some(Value::Source(PixelSource::TileId(id))) => id.to_string(),
347 _ => String::new(),
348 };
349 self.nodeui
350 .set_text_value("actionMaterialTileId", tile_id_text);
351 }
352
353 fn apply(
354 &self,
355 map: &mut Map,
356 _ui: &mut TheUI,
357 _ctx: &mut TheContext,
358 server_ctx: &mut ServerContext,
359 ) -> Option<ProjectUndoAtom> {
360 let prev = map.clone();
361 let mut changed = false;
362
363 for linedef_id in map.selected_linedefs.clone() {
364 changed |= self.apply_linedef_feature(map, linedef_id);
365 }
366
367 if changed {
368 Some(ProjectUndoAtom::MapEdit(
369 server_ctx.pc,
370 Box::new(prev),
371 Box::new(map.clone()),
372 ))
373 } else {
374 None
375 }
376 }
377
378 fn params(&self) -> TheNodeUI {
379 self.nodeui.clone()
380 }
381
382 fn handle_event(
383 &mut self,
384 event: &TheEvent,
385 _project: &mut Project,
386 _ui: &mut TheUI,
387 _ctx: &mut TheContext,
388 _server_ctx: &mut ServerContext,
389 ) -> bool {
390 self.nodeui.handle_event(event)
391 }
392}