1193 lines
38 KiB
Plaintext
1193 lines
38 KiB
Plaintext
#scope_file
|
|
|
|
// @ToDo do a config tweak file thing like jblow has and add this there.
|
|
CAMERA_INACTIVE_TIME_TO_ORBIT :: 200000000.0;
|
|
MAX_CAMERA_DIST :: 25.0;
|
|
MIN_CAMERA_DIST :: 2.0;
|
|
DIST_SCROLL_SPEED :: 0.8;
|
|
|
|
mouse1Active : bool;
|
|
mouse1ActivationPosition : Vector2;
|
|
mouse2Active : bool;
|
|
mouse2ActivationPosition : Vector2;
|
|
mouse3Active : bool;
|
|
mouse3ActivationPosition : Vector2;
|
|
|
|
cameraTilt : float = 0.2;
|
|
cameraDist : float = 10.0;
|
|
cameraRotation : float;
|
|
oldCameraRotation : float;
|
|
oldCameraTilt : float;
|
|
cameraCenter : Vector2;
|
|
|
|
trile_preview_disabled : bool = false;
|
|
|
|
Level_Editor_Tool_Mode :: enum {
|
|
POINT;
|
|
BRUSH;
|
|
AREA;
|
|
LINE;
|
|
INSPECTOR;
|
|
VIEWER;
|
|
ENTITY;
|
|
}
|
|
|
|
current_tool_mode : Level_Editor_Tool_Mode = .POINT;
|
|
brush_radius : int = 2;
|
|
brush_height : int = 0;
|
|
|
|
area_active : bool;
|
|
area_delete : bool;
|
|
area_start_x : int;
|
|
area_start_y : int;
|
|
area_start_z : int;
|
|
|
|
line_active : bool;
|
|
line_delete : bool;
|
|
line_start_x : int;
|
|
line_start_y : int;
|
|
line_start_z : int;
|
|
|
|
current_orientation_face : u8 = 0;
|
|
current_orientation_twist : u8 = 0;
|
|
|
|
inspector_selected : bool;
|
|
inspector_x : s32;
|
|
inspector_y : s32;
|
|
inspector_z : s32;
|
|
inspector_emitter_def : s32 = -1;
|
|
inspector_note_deactivate : bool;
|
|
inspector_note_activate_next : bool;
|
|
|
|
// Entity tool: click to select, drag the gizmo to move or turn, right-click to
|
|
// place. The selection is held by id rather than index so removing an entity
|
|
// can't silently reselect another.
|
|
Entity_Gizmo_Mode :: enum {
|
|
MOVE;
|
|
ROTATE;
|
|
}
|
|
|
|
entity_gizmo : Gizmo_State;
|
|
entity_gizmo_mode : Entity_Gizmo_Mode;
|
|
entity_selected_id : s64 = -1;
|
|
entity_add_type : s32 = -1;
|
|
entity_snap_enabled : bool = true;
|
|
entity_snap_step : float = 1.0;
|
|
|
|
editor_billboards_visible : bool = true;
|
|
|
|
get_current_orientation :: () -> u8 {
|
|
return current_orientation_face * 4 + current_orientation_twist;
|
|
}
|
|
|
|
#scope_export
|
|
|
|
toggle_preview :: () {
|
|
trile_preview_disabled = !trile_preview_disabled;
|
|
} @Command
|
|
|
|
set_dist :: (dist: float) {
|
|
cameraDist = dist;
|
|
} @Command
|
|
|
|
#scope_file
|
|
|
|
tacomaSamples : s32 = 100;
|
|
tacomaResolution : s32 = 500;
|
|
tacoma_tab_scroll : float = 0;
|
|
|
|
lastInputTime : float64;
|
|
|
|
editY : s32;
|
|
|
|
pointerHit : bool;
|
|
pointerX : s32;
|
|
pointerY : s32;
|
|
|
|
show_trile_preview : bool = true;
|
|
trile_preview_x : int = 0;
|
|
trile_preview_y : int = 0;
|
|
trile_preview_z : int = 0;
|
|
|
|
Level_Editor_Tab :: enum {
|
|
TACOMA;
|
|
TOOLS;
|
|
INFO;
|
|
};
|
|
|
|
current_tab : Level_Editor_Tab = .TOOLS;
|
|
get_level_editor_camera :: () -> Camera {
|
|
camera: Camera;
|
|
camera.near = 0.1;
|
|
camera.far = 5000;
|
|
camera.target = .{cameraCenter.x, xx editY, cameraCenter.y};
|
|
cameraDir : Vector3 = .{1, 0, 0};
|
|
qrotation : Quaternion = .{cos(-cameraRotation/2.0),0,sin(-cameraRotation/2.0),0};
|
|
qtilt : Quaternion = .{cos(-cameraTilt/2.0),sin(-cameraTilt/2.0), 0, 0};
|
|
|
|
rotate(*cameraDir, qrotation * qtilt);
|
|
|
|
camera.position = camera.target;
|
|
camera.position += cameraDir * cameraDist;
|
|
|
|
|
|
return camera;
|
|
}
|
|
|
|
|
|
tick_level_editor_camera :: () {
|
|
if console_open_ignore_input then return;
|
|
|
|
if get_time() - lastInputTime > CAMERA_INACTIVE_TIME_TO_ORBIT { // idle rotating camera
|
|
cameraRotation += cast(float) delta_time;
|
|
}
|
|
|
|
cameraSpeed :: 150;
|
|
|
|
forward : Vector3 = .{1, 0, 0};
|
|
qrotation : Quaternion = .{cos(-cameraRotation/2.0),0,sin(-cameraRotation/2.0),0};
|
|
|
|
rotate(*forward, qrotation);
|
|
forward2d := Vector2.{forward.x, forward.z};
|
|
|
|
left : Vector3 = .{0, 0, 1};
|
|
qrotation_left : Quaternion = .{cos(-cameraRotation/2.0),0,sin(-cameraRotation/2.0),0};
|
|
rotate(*left, qrotation_left);
|
|
left2d := Vector2.{left.x, left.z};
|
|
|
|
cameraDist = clamp(cameraDist - mouse_delta_z * DIST_SCROLL_SPEED, 0.0, MAX_CAMERA_DIST);
|
|
|
|
distRange : float = MAX_CAMERA_DIST - MIN_CAMERA_DIST;
|
|
zoomCameraMovementMultiplier := 0.03 + ((cameraDist - MIN_CAMERA_DIST) / distRange) * 0.15;
|
|
|
|
if input_button_states[#char "W"] & .DOWN {
|
|
lastInputTime = get_time();
|
|
cameraCenter += -cameraSpeed * zoomCameraMovementMultiplier * (xx delta_time) * forward2d;
|
|
}
|
|
if input_button_states[#char "S"] & .DOWN {
|
|
lastInputTime = get_time();
|
|
cameraCenter += cameraSpeed * zoomCameraMovementMultiplier * (xx delta_time) * forward2d;
|
|
}
|
|
|
|
if input_button_states[#char "A"] & .DOWN {
|
|
lastInputTime = get_time();
|
|
cameraCenter += -cameraSpeed * zoomCameraMovementMultiplier * (xx delta_time) * left2d;
|
|
}
|
|
if input_button_states[#char "D"] & .DOWN {
|
|
lastInputTime = get_time();
|
|
cameraCenter += cameraSpeed * zoomCameraMovementMultiplier * (xx delta_time) * left2d;
|
|
}
|
|
|
|
if is_action_start(Editor_Action.LEVEL_HEIGHT_UP) {
|
|
lastInputTime = get_time();
|
|
editY = min(editY + 1, 100);
|
|
}
|
|
|
|
if is_action_start(Editor_Action.LEVEL_HEIGHT_DOWN) {
|
|
lastInputTime = get_time();
|
|
editY = max(editY - 1, 0);
|
|
}
|
|
|
|
if current_tool_mode == .VIEWER && get_mouse_state(Key_Code.MOUSE_BUTTON_LEFT) & .DOWN {
|
|
if mouse1Active {
|
|
lastInputTime = get_time();
|
|
diff := mouse1ActivationPosition - Vector2.{input_mouse_x, input_mouse_y};
|
|
diff *= 0.5;
|
|
cameraRotation = oldCameraRotation + diff.x / 100;
|
|
cameraTilt = oldCameraTilt - diff.y / 100;
|
|
cameraTilt = max(0.1, cameraTilt);
|
|
cameraTilt = min(PI/2.2, cameraTilt);
|
|
} else {
|
|
mouse1Active = true;
|
|
mouse1ActivationPosition = Vector2.{input_mouse_x, input_mouse_y};
|
|
oldCameraRotation = cameraRotation;
|
|
oldCameraTilt = cameraTilt;
|
|
}
|
|
} else {
|
|
mouse1Active = false;
|
|
}
|
|
|
|
if get_mouse_state(Key_Code.MOUSE_BUTTON_MIDDLE) & .DOWN {
|
|
if mouse2Active {
|
|
lastInputTime = get_time();
|
|
diff := mouse2ActivationPosition - Vector2.{input_mouse_x, input_mouse_y};
|
|
diff *= 0.5;
|
|
cameraRotation = oldCameraRotation + diff.x / 100;
|
|
cameraTilt = oldCameraTilt - diff.y / 100;
|
|
cameraTilt = max(0.1, cameraTilt);
|
|
cameraTilt = min(PI/2.2, cameraTilt);
|
|
} else {
|
|
mouse2Active = true;
|
|
mouse2ActivationPosition = Vector2.{input_mouse_x, input_mouse_y};
|
|
oldCameraRotation = cameraRotation;
|
|
oldCameraTilt = cameraTilt;
|
|
}
|
|
} else {
|
|
mouse2Active = false;
|
|
}
|
|
|
|
|
|
if get_mouse_state(Key_Code.MOUSE_BUTTON_RIGHT) & .DOWN {
|
|
if mouse3Active {
|
|
lastInputTime = get_time();
|
|
diff := mouse3ActivationPosition - Vector2.{input_mouse_x, input_mouse_y};
|
|
cameraCenter = cameraCenter + forward2d * -diff.y * cast(float) delta_time * zoomCameraMovementMultiplier;
|
|
cameraCenter = cameraCenter + left2d * -diff.x * cast(float) delta_time * zoomCameraMovementMultiplier;
|
|
} else {
|
|
mouse3Active = true;
|
|
mouse3ActivationPosition = Vector2.{input_mouse_x, input_mouse_y};
|
|
}
|
|
} else {
|
|
mouse3Active = false;
|
|
}
|
|
|
|
|
|
}
|
|
|
|
draw_tacoma_tab :: (theme: *GR.Overall_Theme, total_r: GR.Rect) {
|
|
curworld := get_current_world();
|
|
#if FLAG_TACOMA_ENABLED {
|
|
region, inside := GR.begin_scrollable_region(total_r, *theme.scrollable_region_theme);
|
|
r := inside;
|
|
r.y -= tacoma_tab_scroll;
|
|
r.h = ui_h(3,0);
|
|
if GR.button(r, "Render with Tacoma", *theme.button_theme) {
|
|
cam := get_level_editor_camera();
|
|
gen_reference(tacomaResolution, tacomaResolution, cam.position, cam.target, tacomaSamples, true, curworld.world);
|
|
}
|
|
r.y += r.h;
|
|
if GR.button(r, "Bake all chunk RDMs", *theme.button_theme) {
|
|
if curworld.valid then rdm_bake_all_chunks(curworld.world, tacomaSamples, true);
|
|
}
|
|
r.y += r.h;
|
|
if GR.button(r, "Bake all chunk SH grids", *t_button_selectable(theme, !sh_bake.active)) {
|
|
if curworld.valid && !sh_bake.active sh_bake_start(tacomaSamples);
|
|
}
|
|
r.y += r.h;
|
|
if rdm_bake.active {
|
|
total := cast(s32) rdm_bake.jobs.count;
|
|
done := rdm_bake.current_job;
|
|
pct := ifx total > 0 then done * 100 / total else 0;
|
|
GR.label(r, tprint("Baking RDMs: %/% (\%%)", done, total, pct), *t_label_left(theme));
|
|
r.y += r.h;
|
|
}
|
|
if sh_bake.active {
|
|
total := cast(s32) sh_bake.chunk_keys.count;
|
|
done := sh_bake.current_chunk;
|
|
pct := ifx total > 0 then done * 100 / total else 0;
|
|
GR.label(r, tprint("Baking SH grids: %/% (\%%)", done, total, pct), *t_label_left(theme));
|
|
r.y += r.h;
|
|
}
|
|
if current_screenshot.valid {
|
|
aspect := cast(float)current_screenshot.width / cast(float)current_screenshot.height;
|
|
|
|
r.h = r.w / aspect;
|
|
uiTex := New(Ui_Texture,, temp);
|
|
uiTex.tex = current_screenshot.image;
|
|
set_shader_for_images(uiTex);
|
|
immediate_quad(.{r.x, r.y}, .{r.x + r.w, r.y}, .{r.x + r.w, r.y + r.h}, .{r.x, r.y + r.h});
|
|
set_shader_for_color();
|
|
r.y += r.h;
|
|
}
|
|
r.h = ui_h(4,0);
|
|
|
|
GR.label(r, "Samples", *t_label_left(theme));
|
|
r.y += r.h;
|
|
GR.slider(r, *tacomaSamples, 10, 10000, 10, *theme.slider_theme);
|
|
r.y += r.h;
|
|
GR.label(r, "Resolution", *t_label_left(theme));
|
|
r.y += r.h;
|
|
GR.slider(r, *tacomaResolution, 10, 5000, 10, *theme.slider_theme);
|
|
r.y += r.h;
|
|
GR.label(r, tprint("Sky Scale (bake): %", formatFloat(bake_sky_scale, trailing_width=2)), *t_label_left(theme));
|
|
r.y += r.h;
|
|
GR.slider(r, *bake_sky_scale, 0.0, 5.0, 0.05, *theme.slider_theme);
|
|
r.y += r.h;
|
|
GR.label(r, tprint("Sky Desaturation (bake): %", formatFloat(bake_sky_desaturation, trailing_width=2)), *t_label_left(theme));
|
|
r.y += r.h;
|
|
GR.slider(r, *bake_sky_desaturation, 0.0, 1.0, 0.05, *theme.slider_theme);
|
|
r.y += r.h;
|
|
|
|
GR.end_scrollable_region(region, r.x + r.w, r.y, *tacoma_tab_scroll);
|
|
} else {
|
|
r := total_r;
|
|
r.h = ui_h(3,0);
|
|
GR.label(r, "Tacoma is not enabled in this build.", *theme.label_theme);
|
|
}
|
|
}
|
|
|
|
#scope_file
|
|
|
|
Edit_Mode :: enum {
|
|
TRILES;
|
|
}
|
|
|
|
editMode : Edit_Mode;
|
|
|
|
#scope_export
|
|
|
|
draw_tools_tab :: (theme: *GR.Overall_Theme, total_r: GR.Rect) {
|
|
r := total_r;
|
|
r.h = ui_h(4, 0);
|
|
|
|
if keybind_button(r, "Save world", .SAVE, *theme.button_theme) then sworld();
|
|
r.y += r.h * 2;
|
|
|
|
// Tool mode buttons
|
|
if keybind_button(r, "Point", .LEVEL_TOOL_POINT, *t_button_selectable(theme, current_tool_mode == .POINT)) then current_tool_mode = .POINT;
|
|
r.y += r.h;
|
|
if keybind_button(r, "Brush", .LEVEL_TOOL_BRUSH, *t_button_selectable(theme, current_tool_mode == .BRUSH)) then current_tool_mode = .BRUSH;
|
|
r.y += r.h;
|
|
if keybind_button(r, "Area", .LEVEL_TOOL_AREA, *t_button_selectable(theme, current_tool_mode == .AREA)) { current_tool_mode = .AREA; area_active = false; }
|
|
r.y += r.h;
|
|
if keybind_button(r, "Line", .LEVEL_TOOL_LINE, *t_button_selectable(theme, current_tool_mode == .LINE)) { current_tool_mode = .LINE; line_active = false; }
|
|
r.y += r.h;
|
|
if keybind_button(r, "Inspector", .LEVEL_TOOL_INSPECTOR, *t_button_selectable(theme, current_tool_mode == .INSPECTOR)) then current_tool_mode = .INSPECTOR;
|
|
r.y += r.h;
|
|
if keybind_button(r, "Entity", .LEVEL_TOOL_ENTITY, *t_button_selectable(theme, current_tool_mode == .ENTITY)) then current_tool_mode = .ENTITY;
|
|
r.y += r.h;
|
|
if keybind_button(r, "Viewer", .LEVEL_TOOL_VIEWER, *t_button_selectable(theme, current_tool_mode == .VIEWER)) then current_tool_mode = .VIEWER;
|
|
r.y += r.h;
|
|
|
|
// Brush radius/height (only for brush mode)
|
|
if current_tool_mode == .BRUSH {
|
|
r.h = ui_h(3, 2);
|
|
GR.label(r, "Brush Radius", *t_label_left(theme));
|
|
r.y += r.h;
|
|
r.h = ui_h(4, 0);
|
|
GR.slider(r, *brush_radius, 1, 8, 1, *theme.slider_theme);
|
|
r.y += r.h;
|
|
r.h = ui_h(3, 2);
|
|
GR.label(r, "Brush Height (±Y layers)", *t_label_left(theme));
|
|
r.y += r.h;
|
|
r.h = ui_h(4, 0);
|
|
GR.slider(r, *brush_height, 0, 8, 1, *theme.slider_theme);
|
|
r.y += r.h * 2;
|
|
} else {
|
|
r.y += r.h;
|
|
}
|
|
|
|
// Status hints for multi-click tools
|
|
r.h = ui_h(3, 2);
|
|
if current_tool_mode == .AREA {
|
|
if area_active {
|
|
GR.label(r, "Click second corner to fill", *t_label_left(theme));
|
|
} else {
|
|
GR.label(r, "Click first corner to start", *t_label_left(theme));
|
|
}
|
|
r.y += r.h;
|
|
} else if current_tool_mode == .LINE {
|
|
if line_active {
|
|
GR.label(r, "Click endpoint to draw line", *t_label_left(theme));
|
|
} else {
|
|
GR.label(r, "Click start of line", *t_label_left(theme));
|
|
}
|
|
r.y += r.h;
|
|
} else if current_tool_mode == .INSPECTOR || current_tool_mode == .ENTITY {
|
|
r.h = ui_h(3, 2);
|
|
if GR.button(r, ifx editor_billboards_visible then "Hide Markers" else "Show Markers", *theme.button_theme, 199) {
|
|
editor_billboards_visible = !editor_billboards_visible;
|
|
}
|
|
r.y += r.h;
|
|
}
|
|
|
|
// Orientation controls
|
|
r.y += r.h;
|
|
r.h = ui_h(3, 2);
|
|
GR.label(r, "-- Orientation --", *t_label_left(theme));
|
|
r.y += r.h;
|
|
twist_angle := current_orientation_twist * 90;
|
|
GR.label(r, tprint("Face: % Twist: %°", current_orientation_face, twist_angle), *t_label_left(theme));
|
|
r.y += r.h;
|
|
GR.label(r, tprint("%/%: twist %: face",
|
|
key_combo_string(get_action_combo(Editor_Action.LEVEL_TWIST_CCW)),
|
|
key_combo_string(get_action_combo(Editor_Action.LEVEL_TWIST_CW)),
|
|
key_combo_string(get_action_combo(Editor_Action.LEVEL_CYCLE_FACE))), *t_label_left(theme));
|
|
r.y += r.h;
|
|
}
|
|
|
|
handle_tool_click :: (x: int, y: int, z: int, delete: bool = false) {
|
|
curworld := get_current_world();
|
|
if delete {
|
|
remove_trile(cast(s32)x, cast(s32)y, cast(s32)z);
|
|
} else {
|
|
if editor_current_trile != null then add_trile(editor_current_trile.name, cast(s32)x, cast(s32)y, cast(s32)z, get_current_orientation());
|
|
}
|
|
}
|
|
|
|
apply_brush :: (cx: int, cy: int, cz: int, delete: bool) {
|
|
for dy: -brush_height..brush_height {
|
|
for dx: -brush_radius..brush_radius {
|
|
for dz: -brush_radius..brush_radius {
|
|
dist := sqrt(cast(float)(dx*dx + dz*dz));
|
|
if dist <= cast(float)brush_radius {
|
|
handle_tool_click(cx + dx, cy + dy, cz + dz, delete);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
apply_area :: (x2: int, y2: int, z2: int, delete: bool) {
|
|
for x: min(area_start_x, x2)..max(area_start_x, x2) {
|
|
for y: min(area_start_y, y2)..max(area_start_y, y2) {
|
|
for z: min(area_start_z, z2)..max(area_start_z, z2) {
|
|
handle_tool_click(x, y, z, delete);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
apply_line :: (x2: int, y2: int, z2: int, delete: bool) {
|
|
x1 := line_start_x; y1 := line_start_y; z1 := line_start_z;
|
|
dx := abs(x2 - x1); dy := abs(y2 - y1); dz := abs(z2 - z1);
|
|
sx := ifx x1 < x2 then 1 else -1;
|
|
sy := ifx y1 < y2 then 1 else -1;
|
|
sz := ifx z1 < z2 then 1 else -1;
|
|
// 3D Bresenham: drive along the longest axis
|
|
dm := max(dx, max(dy, dz));
|
|
x := x1; y := y1; z := z1;
|
|
ex := dm / 2; ey := dm / 2; ez := dm / 2;
|
|
count := 0;
|
|
while count <= dm && count < 2000 {
|
|
handle_tool_click(x, y, z, delete);
|
|
ex -= dx; if ex < 0 { x += sx; ex += dm; }
|
|
ey -= dy; if ey < 0 { y += sy; ey += dm; }
|
|
ez -= dz; if ez < 0 { z += sz; ez += dm; }
|
|
count += 1;
|
|
}
|
|
}
|
|
|
|
add_trile :: (name: string, x: s32, y: s32, z: s32, orientation: u8 = 0) {
|
|
curworld := get_current_world();
|
|
|
|
// Remove any existing trile at this position first.
|
|
remove_trile(x, y, z);
|
|
|
|
// Find or create the chunk.
|
|
key := world_to_chunk_coord(x, y, z);
|
|
chunk := table_find_pointer(*curworld.world.chunks, key);
|
|
if !chunk {
|
|
new_chunk: Chunk;
|
|
new_chunk.coord = key;
|
|
table_set(*curworld.world.chunks, key, new_chunk);
|
|
chunk = table_find_pointer(*curworld.world.chunks, key);
|
|
}
|
|
|
|
lx, ly, lz := world_to_local(x, y, z);
|
|
inst := Trile_Instance.{x = lx, y = ly, z = lz, orientation = orientation};
|
|
|
|
// Find existing group for this trile type, or create one.
|
|
added := false;
|
|
for *group: chunk.groups {
|
|
if group.trile_name == name {
|
|
array_add(*group.instances, inst);
|
|
added = true;
|
|
break;
|
|
}
|
|
}
|
|
if !added {
|
|
group: Chunk_Trile_Group;
|
|
group.trile_name = sprint("%", name);
|
|
array_add(*group.instances, inst);
|
|
array_add(*chunk.groups, group);
|
|
}
|
|
upload_chunk(chunk);
|
|
trile, ok := get_trile(name);
|
|
if !ok || trile.skip_collision then return;
|
|
chunk_set_occupancy_mask_at(chunk, lx, ly, lz, occupancy_state_for(trile, orientation));
|
|
} @Command
|
|
|
|
remove_trile :: (x: s32, y: s32, z: s32) {
|
|
curworld := get_current_world();
|
|
|
|
key := world_to_chunk_coord(x, y, z);
|
|
chunk := table_find_pointer(*curworld.world.chunks, key);
|
|
if !chunk then return;
|
|
|
|
lx, ly, lz := world_to_local(x, y, z);
|
|
|
|
removed := false;
|
|
for *group: chunk.groups {
|
|
for inst, idx: group.instances {
|
|
if inst.x == lx && inst.y == ly && inst.z == lz {
|
|
array_unordered_remove_by_index(*group.instances, idx);
|
|
removed = true;
|
|
break;
|
|
}
|
|
}
|
|
if removed break;
|
|
}
|
|
if removed upload_chunk(chunk);
|
|
chunk_set_occupancy_mask_at(chunk, lx, ly, lz, .FREE);
|
|
} @Command
|
|
|
|
|
|
|
|
level_editor_clear_entity_selection :: () {
|
|
entity_selected_id = -1;
|
|
entity_gizmo.hover = .NONE;
|
|
entity_gizmo.active = .NONE;
|
|
}
|
|
|
|
get_selected_entity :: (world: *World) -> *Entity {
|
|
if entity_selected_id < 0 then return null;
|
|
for e: world.entities if cast(s64)e.id == entity_selected_id return e;
|
|
return null;
|
|
}
|
|
|
|
// World-space box covering an entity's cell plus every part it draws, used for
|
|
// click-picking and for the selection outline.
|
|
entity_bounds :: (e: *Entity) -> (mn: Vector3, mx: Vector3) {
|
|
mn := e.position;
|
|
mx := e.position + Vector3.{1, 1, 1};
|
|
for part: get_entity_parts(e.type) {
|
|
p, q : Vector3 = ---;
|
|
if part.kind == .TRILE {
|
|
p = entity_trile_point(e, part.offset);
|
|
q = p + Vector3.{1, 1, 1};
|
|
} else {
|
|
// Billboards and emitters are points; give them enough of a box to click.
|
|
c := entity_point(e, part.offset);
|
|
p = c - Vector3.{0.25, 0.25, 0.25};
|
|
q = c + Vector3.{0.25, 0.25, 0.25};
|
|
}
|
|
mn.x = min(mn.x, p.x); mn.y = min(mn.y, p.y); mn.z = min(mn.z, p.z);
|
|
mx.x = max(mx.x, q.x); mx.y = max(mx.y, q.y); mx.z = max(mx.z, q.z);
|
|
}
|
|
return mn, mx;
|
|
}
|
|
|
|
// The billboard that stands in for an entity in the editor: the type's declared
|
|
// EDITOR_MARKER, or a default one when the entity draws nothing you could aim
|
|
// at. Null for entities that are already visible on their own.
|
|
entity_marker_animation :: (e: *Entity) -> *Animation {
|
|
marker := get_entity_editor_marker(e.type);
|
|
if marker.count > 0 return get_animation_from_string(marker);
|
|
for part: get_entity_parts(e.type) {
|
|
if part.kind == .TRILE || part.kind == .BILLBOARD return null;
|
|
}
|
|
return get_animation_from_string("game_core.ball");
|
|
}
|
|
|
|
pick_entity :: (world: *World, ray: Ray) -> *Entity {
|
|
best : *Entity = null;
|
|
best_dist : float = FLOAT32_MAX;
|
|
for e: world.entities {
|
|
mn, mx := entity_bounds(e);
|
|
col := does_ray_hit_cube(ray, .{position = mn, size = mx - mn});
|
|
if !col.hit then continue;
|
|
d := abs(col.distance);
|
|
if d < best_dist {
|
|
best_dist = d;
|
|
best = e;
|
|
}
|
|
}
|
|
return best;
|
|
}
|
|
|
|
snap_to_step :: (v: float, step: float) -> float {
|
|
if step <= 0 then return v;
|
|
return floor(v / step + 0.5) * step;
|
|
}
|
|
|
|
tick_entity_tool :: (ray: Ray) {
|
|
curworld := get_current_world();
|
|
if !curworld.valid then return;
|
|
world := *curworld.world;
|
|
|
|
e := get_selected_entity(world);
|
|
if e == null {
|
|
entity_gizmo.hover = .NONE;
|
|
entity_gizmo.active = .NONE;
|
|
} else {
|
|
mn, mx := entity_bounds(e);
|
|
debug_aabb_3d_overlay(mn, mx, .{1.0, 0.75, 0.1, 0.8}, 2.0);
|
|
|
|
// The gizmo sits at the middle of the entity's cell, which is both what
|
|
// the entity's marker draws on and what it rotates about.
|
|
cam := get_level_editor_camera();
|
|
origin := e.position + ENTITY_PIVOT;
|
|
if entity_gizmo_mode == .MOVE {
|
|
moved, changed := gizmo_translate(*entity_gizmo, origin, cam, ray);
|
|
if changed {
|
|
p := moved - ENTITY_PIVOT;
|
|
if entity_snap_enabled {
|
|
p.x = snap_to_step(p.x, entity_snap_step);
|
|
p.y = snap_to_step(p.y, entity_snap_step);
|
|
p.z = snap_to_step(p.z, entity_snap_step);
|
|
}
|
|
e.position = p;
|
|
}
|
|
} else {
|
|
u, v, n, steps := gizmo_rotate(*entity_gizmo, origin, cam, ray);
|
|
if steps != 0 {
|
|
// A negative drag is the same turn taken the other way round.
|
|
turn := ifx steps > 0 then orientation_quarter_turn(u, v, n) else orientation_quarter_turn(v, u, n);
|
|
for 1..abs(steps) e.orientation = compose_orientations(turn, e.orientation);
|
|
}
|
|
}
|
|
for *inst: e.emitters inst.position = entity_point(e, inst.offset);
|
|
}
|
|
|
|
// A click that didn't land on the gizmo selects (or clears) the selection.
|
|
if entity_gizmo.active == .NONE && entity_gizmo.hover == .NONE {
|
|
if get_mouse_state(Key_Code.MOUSE_BUTTON_LEFT) & .START {
|
|
picked := pick_entity(world, ray);
|
|
entity_selected_id = ifx picked then cast(s64)picked.id else cast(s64)-1;
|
|
}
|
|
}
|
|
}
|
|
|
|
editor_edit_y :: () -> float {
|
|
y := cast(float)editY;
|
|
curworld := get_current_world();
|
|
if curworld.valid {
|
|
ph := effective_plane_height(*curworld.world.conf);
|
|
if ph > y && ph < y + 1.0 then y = ph + 0.01;
|
|
}
|
|
return y;
|
|
}
|
|
|
|
tick_level_editor :: () {
|
|
#if FLAG_TACOMA_ENABLED { rdm_bake_tick(); sh_bake_tick(); }
|
|
tick_level_editor_camera();
|
|
tick_particles(cast(float)delta_time);
|
|
|
|
curworld := get_current_world();
|
|
if curworld.valid then tick_entity_emitters(*curworld.world, cast(float)delta_time);
|
|
|
|
if is_action_start(Editor_Action.LEVEL_TWIST_CCW) {
|
|
lastInputTime = get_time();
|
|
current_orientation_twist = (current_orientation_twist + 1) % 4;
|
|
}
|
|
if is_action_start(Editor_Action.LEVEL_TWIST_CW) {
|
|
lastInputTime = get_time();
|
|
current_orientation_twist = (current_orientation_twist + 3) % 4;
|
|
}
|
|
if is_action_start(Editor_Action.LEVEL_CYCLE_FACE) {
|
|
lastInputTime = get_time();
|
|
current_orientation_face = (current_orientation_face + 1) % 6;
|
|
}
|
|
|
|
cam := get_level_editor_camera();
|
|
ray := get_mouse_ray(*cam);
|
|
hit, point := ray_plane_collision_point(ray, editor_edit_y(), 20);
|
|
|
|
// The entity tool works off the entities themselves, not the edit plane, so
|
|
// it runs whether or not the cursor is over the plane.
|
|
if current_tool_mode == .ENTITY then tick_entity_tool(ray);
|
|
|
|
show_trile_preview = false;
|
|
if hit {
|
|
show_trile_preview = true;
|
|
trile_preview_x = xx floor(point.x);
|
|
trile_preview_y = editY;
|
|
trile_preview_z = xx floor(point.y);
|
|
|
|
px := trile_preview_x;
|
|
py := trile_preview_y;
|
|
pz := trile_preview_z;
|
|
|
|
if current_tool_mode == .POINT {
|
|
if get_mouse_state(Key_Code.MOUSE_BUTTON_LEFT) & .START {
|
|
handle_tool_click(px, py, pz);
|
|
}
|
|
if get_mouse_state(Key_Code.MOUSE_BUTTON_RIGHT) & .START {
|
|
handle_tool_click(px, py, pz, true);
|
|
}
|
|
} else if current_tool_mode == .BRUSH {
|
|
if get_mouse_state(Key_Code.MOUSE_BUTTON_LEFT) & .DOWN {
|
|
apply_brush(px, py, pz, false);
|
|
}
|
|
if get_mouse_state(Key_Code.MOUSE_BUTTON_RIGHT) & .DOWN {
|
|
apply_brush(px, py, pz, true);
|
|
}
|
|
} else if current_tool_mode == .AREA {
|
|
if get_mouse_state(Key_Code.MOUSE_BUTTON_LEFT) & .START {
|
|
if !area_active {
|
|
area_active = true;
|
|
area_delete = false;
|
|
area_start_x = px;
|
|
area_start_y = py;
|
|
area_start_z = pz;
|
|
} else if !area_delete {
|
|
apply_area(px, py, pz, false);
|
|
area_active = false;
|
|
} else {
|
|
area_active = false; // cancel delete selection
|
|
}
|
|
}
|
|
if get_mouse_state(Key_Code.MOUSE_BUTTON_RIGHT) & .START {
|
|
if !area_active {
|
|
area_active = true;
|
|
area_delete = true;
|
|
area_start_x = px;
|
|
area_start_y = py;
|
|
area_start_z = pz;
|
|
} else if area_delete {
|
|
apply_area(px, py, pz, true);
|
|
area_active = false;
|
|
} else {
|
|
area_active = false; // cancel add selection
|
|
}
|
|
}
|
|
} else if current_tool_mode == .LINE {
|
|
if get_mouse_state(Key_Code.MOUSE_BUTTON_LEFT) & .START {
|
|
if !line_active {
|
|
line_active = true;
|
|
line_delete = false;
|
|
line_start_x = px;
|
|
line_start_y = py;
|
|
line_start_z = pz;
|
|
} else if !line_delete {
|
|
apply_line(px, py, pz, false);
|
|
line_active = false;
|
|
} else {
|
|
line_active = false; // cancel delete selection
|
|
}
|
|
}
|
|
if get_mouse_state(Key_Code.MOUSE_BUTTON_RIGHT) & .START {
|
|
if !line_active {
|
|
line_active = true;
|
|
line_delete = true;
|
|
line_start_x = px;
|
|
line_start_y = py;
|
|
line_start_z = pz;
|
|
} else if line_delete {
|
|
apply_line(px, py, pz, true);
|
|
line_active = false;
|
|
} else {
|
|
line_active = false; // cancel add selection
|
|
}
|
|
}
|
|
} else if current_tool_mode == .INSPECTOR {
|
|
if get_mouse_state(Key_Code.MOUSE_BUTTON_LEFT) & .START {
|
|
inspector_selected = true;
|
|
inspector_x = cast(s32)px;
|
|
inspector_y = cast(s32)py;
|
|
inspector_z = cast(s32)pz;
|
|
inspector_note_deactivate = true;
|
|
}
|
|
} else if current_tool_mode == .ENTITY {
|
|
// Right-click places the type picked in the panel, so the cursor never
|
|
// has to leave the spot you're aiming at.
|
|
if get_mouse_state(Key_Code.MOUSE_BUTTON_RIGHT) & .START {
|
|
add_entity_at(cast(s32)px, cast(s32)py, cast(s32)pz);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
draw_level_editor_preview :: (view: View_Id) {
|
|
curworld := get_current_world();
|
|
if !curworld.valid then return;
|
|
|
|
px := trile_preview_x; py := trile_preview_y; pz := trile_preview_z;
|
|
|
|
// The entity tool never paints triles, so it shows no brush preview.
|
|
if current_tool_mode == .ENTITY then return;
|
|
|
|
if current_tool_mode == .INSPECTOR {
|
|
name, orientation, found := get_trile_at(*curworld.world, xx px, xx py, xx pz);
|
|
cursor_trile := ifx found then name else (ifx editor_current_trile then editor_current_trile.name else "");
|
|
mesh, ok := get_trile_mesh(cursor_trile);
|
|
if ok draw_trile(view, mesh, .{xx px, xx py, xx pz}, orientation, .ADD_PREVIEW);
|
|
return;
|
|
}
|
|
|
|
if !editor_current_trile then return;
|
|
|
|
positions: [..]Vector4;
|
|
positions.allocator = temp;
|
|
ori_u8 := get_current_orientation();
|
|
ori := cast(float) ori_u8;
|
|
|
|
if current_tool_mode == .POINT {
|
|
array_add(*positions, .{cast(float)px, cast(float)py, cast(float)pz, ori});
|
|
} else if current_tool_mode == .BRUSH {
|
|
for dy: -brush_height..brush_height {
|
|
for dx: -brush_radius..brush_radius {
|
|
for dz: -brush_radius..brush_radius {
|
|
dist := sqrt(cast(float)(dx*dx + dz*dz));
|
|
if dist <= cast(float)brush_radius {
|
|
array_add(*positions, .{cast(float)(px+dx), cast(float)(py+dy), cast(float)(pz+dz), ori});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if current_tool_mode == .AREA {
|
|
if area_active {
|
|
for x: min(area_start_x, px)..max(area_start_x, px) {
|
|
for y: min(area_start_y, py)..max(area_start_y, py) {
|
|
for z: min(area_start_z, pz)..max(area_start_z, pz) {
|
|
array_add(*positions, .{cast(float)x, cast(float)y, cast(float)z, ori});
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// array_add(*positions, .{cast(float)px, cast(float)py, cast(float)pz, ori});
|
|
}
|
|
} else if current_tool_mode == .LINE {
|
|
if line_active {
|
|
x1 := line_start_x; y1 := line_start_y; z1 := line_start_z;
|
|
x2 := px; y2 := py; z2 := pz;
|
|
dx := abs(x2-x1); dy := abs(y2-y1); dz := abs(z2-z1);
|
|
sx := ifx x1 < x2 then 1 else -1;
|
|
sy := ifx y1 < y2 then 1 else -1;
|
|
sz := ifx z1 < z2 then 1 else -1;
|
|
dm := max(dx, max(dy, dz));
|
|
x := x1; y := y1; z := z1;
|
|
ex := dm/2; ey := dm/2; ez := dm/2;
|
|
count := 0;
|
|
while count <= dm && count < 2000 {
|
|
array_add(*positions, .{cast(float)x, cast(float)y, cast(float)z, ori});
|
|
ex -= dx; if ex < 0 { x += sx; ex += dm; }
|
|
ey -= dy; if ey < 0 { y += sy; ey += dm; }
|
|
ez -= dz; if ez < 0 { z += sz; ez += dm; }
|
|
count += 1;
|
|
}
|
|
} else {
|
|
// array_add(*positions, .{cast(float)px, cast(float)py, cast(float)pz, ori});
|
|
}
|
|
}
|
|
|
|
if positions.count == 0 then return;
|
|
|
|
is_delete := (current_tool_mode == .AREA && area_active && area_delete)
|
|
|| (current_tool_mode == .LINE && line_active && line_delete);
|
|
|
|
mesh, ok := get_trile_mesh(editor_current_trile.name);
|
|
if !ok then return;
|
|
tint := ifx is_delete then Trile_Tint.DELETE_PREVIEW else .ADD_PREVIEW;
|
|
for positions draw_trile(view, mesh, it.xyz, ori_u8, tint);
|
|
}
|
|
|
|
get_trile_at :: (world: *World, wx: s32, wy: s32, wz: s32) -> (name: string, orientation: u8, found: bool) {
|
|
key := world_to_chunk_coord(wx, wy, wz);
|
|
chunk := table_find_pointer(*world.chunks, key);
|
|
if !chunk then return "", 0, false;
|
|
lx, ly, lz := world_to_local(wx, wy, wz);
|
|
for group: chunk.groups {
|
|
for inst: group.instances {
|
|
if inst.x == lx && inst.y == ly && inst.z == lz {
|
|
return group.trile_name, inst.orientation, true;
|
|
}
|
|
}
|
|
}
|
|
return "", 0, false;
|
|
}
|
|
|
|
find_emitter_at :: (world: *World, x: s32, y: s32, z: s32) -> (idx: s32, found: bool) {
|
|
for inst, i: world.emitter_instances {
|
|
ix := cast(s32)floor(inst.position.x);
|
|
iy := cast(s32)floor(inst.position.y);
|
|
iz := cast(s32)floor(inst.position.z);
|
|
if ix == x && iy == y && iz == z then return cast(s32)i, true;
|
|
}
|
|
return -1, false;
|
|
}
|
|
|
|
find_note_at :: (world: *World, x: s32, y: s32, z: s32) -> (idx: s32, found: bool) {
|
|
for note, i: world.notes {
|
|
if note.position.x == x && note.position.y == y && note.position.z == z then return cast(s32)i, true;
|
|
}
|
|
return -1, false;
|
|
}
|
|
|
|
draw_inspector_panel :: (r: *GR.Rect, theme: *GR.Overall_Theme) {
|
|
curworld := get_current_world();
|
|
if !curworld.valid then return;
|
|
world := *curworld.world;
|
|
|
|
r.h = ui_h(3, 2);
|
|
GR.label(r.*, tprint("Slot: (%, %, %)", inspector_x, inspector_y, inspector_z), *t_label_left(theme));
|
|
r.y += r.h;
|
|
|
|
trile_name, orientation, has_trile := get_trile_at(world, inspector_x, inspector_y, inspector_z);
|
|
if has_trile {
|
|
GR.label(r.*, tprint("Trile: % orient: %", trile_name, orientation), *t_label_left(theme));
|
|
r.y += r.h;
|
|
} else {
|
|
GR.label(r.*, "Trile: (empty)", *t_label_left(theme));
|
|
r.y += r.h;
|
|
}
|
|
|
|
r.y += r.h * 0.5;
|
|
GR.label(r.*, "-- Emitter --", *t_label_left(theme));
|
|
r.y += r.h;
|
|
|
|
emitter_idx, has_emitter := find_emitter_at(world, inspector_x, inspector_y, inspector_z);
|
|
if has_emitter {
|
|
inst := *world.emitter_instances[emitter_idx];
|
|
GR.label(r.*, tprint("Def: %", inst.definition_name), *t_label_left(theme));
|
|
r.y += r.h;
|
|
r.h = ui_h(4, 0);
|
|
GR.label(r.*, tprint("Offset X: %", formatFloat(inst.offset.x, trailing_width=2)), *t_label_left(theme));
|
|
r.y += r.h;
|
|
GR.slider(r.*, *inst.offset.x, -0.5, 0.5, 0.05, *theme.slider_theme);
|
|
r.y += r.h;
|
|
GR.label(r.*, tprint("Offset Y: %", formatFloat(inst.offset.y, trailing_width=2)), *t_label_left(theme));
|
|
r.y += r.h;
|
|
GR.slider(r.*, *inst.offset.y, -0.5, 0.5, 0.05, *theme.slider_theme);
|
|
r.y += r.h;
|
|
GR.label(r.*, tprint("Offset Z: %", formatFloat(inst.offset.z, trailing_width=2)), *t_label_left(theme));
|
|
r.y += r.h;
|
|
GR.slider(r.*, *inst.offset.z, -0.5, 0.5, 0.05, *theme.slider_theme);
|
|
r.y += r.h;
|
|
if GR.button(r.*, "Remove Emitter", *theme.button_theme, 300) {
|
|
array_ordered_remove_by_index(*world.emitter_instances, emitter_idx);
|
|
}
|
|
r.y += r.h;
|
|
} else {
|
|
r.h = ui_h(4, 0);
|
|
GR.label(r.*, "Select emitter def:", *t_label_left(theme));
|
|
r.y += r.h;
|
|
for def, idx: g_emitter_defs {
|
|
selected := (cast(s32)idx == inspector_emitter_def);
|
|
if GR.button(r.*, def.name, *t_button_selectable(theme, selected), cast(s32)(200 + idx)) {
|
|
inspector_emitter_def = cast(s32)idx;
|
|
}
|
|
r.y += r.h;
|
|
}
|
|
if inspector_emitter_def >= 0 && inspector_emitter_def < cast(s32)g_emitter_defs.count {
|
|
if GR.button(r.*, "Add Emitter", *theme.button_theme, 301) {
|
|
def := *g_emitter_defs[inspector_emitter_def];
|
|
inst : Particle_Emitter_Instance;
|
|
inst.definition = def;
|
|
inst.definition_name = def.name;
|
|
inst.position = .{cast(float)inspector_x + 0.5, cast(float)inspector_y, cast(float)inspector_z + 0.5};
|
|
inst.active = true;
|
|
array_add(*world.emitter_instances, inst);
|
|
}
|
|
r.y += r.h;
|
|
}
|
|
}
|
|
|
|
r.h = ui_h(3, 2);
|
|
r.y += r.h * 0.5;
|
|
GR.label(r.*, "-- Note --", *t_label_left(theme));
|
|
r.y += r.h;
|
|
|
|
note_idx, has_note := find_note_at(world, inspector_x, inspector_y, inspector_z);
|
|
note_input_action : GR.Text_Input_Action;
|
|
if inspector_note_activate_next {
|
|
note_input_action = .ACTIVATE;
|
|
inspector_note_activate_next = false;
|
|
} else if inspector_note_deactivate {
|
|
note_input_action = .DEACTIVATE;
|
|
inspector_note_deactivate = false;
|
|
}
|
|
if has_note {
|
|
note := *world.notes[note_idx];
|
|
r.h = ui_h(4, 0);
|
|
note_id := cast(s64)inspector_x * 1000003 + cast(s64)inspector_y * 1009 + cast(s64)inspector_z;
|
|
a, _, note_state := GR.text_input(r.*, note.text, *theme.text_input_theme, note_id, input_action = note_input_action);
|
|
if (a & .TEXT_MODIFIED) || (a & .ENTERED) note.text = copy_string(note_state.text);
|
|
r.y += r.h;
|
|
if GR.button(r.*, "Remove Note", *theme.button_theme, 302) {
|
|
array_ordered_remove_by_index(*world.notes, note_idx);
|
|
}
|
|
r.y += r.h;
|
|
} else {
|
|
r.h = ui_h(4, 0);
|
|
if GR.button(r.*, "Add Note", *theme.button_theme, 303) {
|
|
note : Editor_Note;
|
|
note.position = .{inspector_x, inspector_y, inspector_z};
|
|
note.text = "";
|
|
array_add(*world.notes, note);
|
|
inspector_note_activate_next = true;
|
|
}
|
|
r.y += r.h;
|
|
}
|
|
|
|
}
|
|
|
|
draw_entity_tool_panel :: (r: *GR.Rect, theme: *GR.Overall_Theme) {
|
|
curworld := get_current_world();
|
|
if !curworld.valid then return;
|
|
world := *curworld.world;
|
|
|
|
r.h = ui_h(3, 2);
|
|
GR.label(r.*, "-- Entity Tool --", *t_label_left(theme));
|
|
r.y += r.h;
|
|
|
|
e := get_selected_entity(world);
|
|
if e != null {
|
|
GR.label(r.*, tprint("Type: %", entity_type_name(e.type)), *t_label_left(theme));
|
|
r.y += r.h;
|
|
GR.label(r.*, tprint("Pos: %, %, %",
|
|
formatFloat(e.position.x, trailing_width=2),
|
|
formatFloat(e.position.y, trailing_width=2),
|
|
formatFloat(e.position.z, trailing_width=2)), *t_label_left(theme));
|
|
r.y += r.h;
|
|
|
|
GR.label(r.*, tprint("Orientation: %", e.orientation), *t_label_left(theme));
|
|
r.y += r.h;
|
|
|
|
r.h = ui_h(4, 0);
|
|
mode_r := r.*;
|
|
mode_r.w = r.w / 2;
|
|
if GR.button(mode_r, "Move", *t_button_selectable(theme, entity_gizmo_mode == .MOVE), 524) {
|
|
entity_gizmo_mode = .MOVE;
|
|
entity_gizmo.active = .NONE;
|
|
}
|
|
mode_r.x += mode_r.w;
|
|
if GR.button(mode_r, "Rotate", *t_button_selectable(theme, entity_gizmo_mode == .ROTATE), 525) {
|
|
entity_gizmo_mode = .ROTATE;
|
|
entity_gizmo.active = .NONE;
|
|
}
|
|
r.y += r.h;
|
|
|
|
// Rotation is always in quarter turns, so snapping is a move-only setting.
|
|
if entity_gizmo_mode == .MOVE {
|
|
snap_label := ifx entity_snap_enabled then tprint("Snap: %", formatFloat(entity_snap_step, trailing_width=2)) else "Snap: off";
|
|
if GR.button(r.*, snap_label, *t_button_selectable(theme, entity_snap_enabled), 520) {
|
|
entity_snap_enabled = !entity_snap_enabled;
|
|
}
|
|
r.y += r.h;
|
|
if entity_snap_enabled {
|
|
GR.slider(r.*, *entity_snap_step, 0.125, 1.0, 0.125, *theme.slider_theme);
|
|
r.y += r.h;
|
|
}
|
|
}
|
|
|
|
fields_r := r.*;
|
|
fields_r.h = ui_h(30, 0);
|
|
entity_autoedit(fields_r, e, theme);
|
|
r.y += fields_r.h;
|
|
|
|
if GR.button(r.*, "Remove Entity", *theme.button_theme, 521) {
|
|
remove_entity(world, e);
|
|
level_editor_clear_entity_selection();
|
|
}
|
|
r.y += r.h;
|
|
if GR.button(r.*, "Deselect", *theme.button_theme, 522) {
|
|
level_editor_clear_entity_selection();
|
|
}
|
|
r.y += r.h;
|
|
} else {
|
|
GR.label(r.*, "Click an entity to select it.", *t_label_left(theme));
|
|
r.y += r.h;
|
|
GR.label(r.*, "Drag the gizmo to move or turn.", *t_label_left(theme));
|
|
r.y += r.h;
|
|
}
|
|
|
|
r.h = ui_h(3, 2);
|
|
r.y += r.h * 0.5;
|
|
GR.label(r.*, "-- Spawn --", *t_label_left(theme));
|
|
r.y += r.h;
|
|
|
|
r.h = ui_h(4, 0);
|
|
for info, idx: ENTITY_TYPE_TABLE {
|
|
selected := cast(s32)idx == entity_add_type;
|
|
if GR.button(r.*, info.name, *t_button_selectable(theme, selected), cast(s32)(500 + idx)) {
|
|
entity_add_type = cast(s32)idx;
|
|
}
|
|
r.y += r.h;
|
|
}
|
|
r.h = ui_h(3, 2);
|
|
if entity_add_type >= 0 && entity_add_type < cast(s32)ENTITY_TYPE_TABLE.count {
|
|
GR.label(r.*, "Right-click in the world to place.", *t_label_left(theme));
|
|
} else {
|
|
GR.label(r.*, "Pick a type to place.", *t_label_left(theme));
|
|
}
|
|
r.y += r.h;
|
|
}
|
|
|
|
// Places the type picked in the panel. Called from the right-click handler, so
|
|
// the position is whatever cell the cursor is over on the edit plane.
|
|
add_entity_at :: (x: s32, y: s32, z: s32) {
|
|
if entity_add_type < 0 || entity_add_type >= cast(s32)ENTITY_TYPE_TABLE.count then return;
|
|
curworld := get_current_world();
|
|
if !curworld.valid then return;
|
|
spawned := spawn_entity(*curworld.world, ENTITY_TYPE_TABLE[entity_add_type].name, .{cast(float)x, cast(float)y, cast(float)z});
|
|
if spawned then entity_selected_id = cast(s64)spawned.id;
|
|
}
|
|
|
|
// Runs autoedit on the entity's concrete type, so each type gets an editor
|
|
// generated from its own fields.
|
|
entity_autoedit :: (rect: GR.Rect, e: *Entity, theme: *GR.Overall_Theme) {
|
|
#insert #run,stallable gen_entity_dispatch("autoedit(rect, cast(*%)e, theme, 400)");
|
|
}
|
|
|
|
draw_editor_billboards :: (view: View_Id) {
|
|
if !editor_billboards_visible then return;
|
|
curworld := get_current_world();
|
|
if !curworld.valid then return;
|
|
|
|
// Entities are marked first, before the fallback animation is required, so a
|
|
// type with its own EDITOR_MARKER still shows up if "game_core.ball" is gone.
|
|
for e: curworld.world.entities {
|
|
marker := entity_marker_animation(e);
|
|
if marker == null then continue;
|
|
draw_animation_frame(view, marker, 0, e.position + ENTITY_PIVOT);
|
|
}
|
|
|
|
anim := get_animation_from_string("game_core.ball");
|
|
if anim == null then return;
|
|
|
|
for inst: curworld.world.emitter_instances {
|
|
draw_animation_frame(view, anim, 0, inst.position);
|
|
}
|
|
|
|
for note: curworld.world.notes {
|
|
draw_animation_frame(view, anim, 0, .{cast(float)note.position.x + 0.5, cast(float)note.position.y + 0.5, cast(float)note.position.z + 0.5});
|
|
}
|
|
}
|
|
|
|
draw_level_editor :: () {
|
|
curworld := get_current_world();
|
|
if !curworld.valid then return;
|
|
view := add_view(.{camera = get_level_editor_camera(), environment = world_environment(*curworld.world)});
|
|
draw_entities(view, *curworld.world);
|
|
draw_live_particles(view);
|
|
if show_trile_preview && !trile_preview_disabled {
|
|
draw_level_editor_preview(view);
|
|
}
|
|
draw_editor_billboards(view);
|
|
debug_grid(floor(cameraCenter.x), editor_edit_y(), floor(cameraCenter.y), 20, .{0.3, 0.3, 0.35, 0.7});
|
|
}
|
|
|
|
draw_level_editor_ui :: (theme: *GR.Overall_Theme) {
|
|
r := GR.get_rect(0, ui_h(5,0), ui_w(20, 20), ui_h(95, 0));
|
|
ui_add_mouse_occluder(r);
|
|
draw_bg_rectangle(r, theme);
|
|
tab_r := r;
|
|
tab_r.h = ui_h(3,0);
|
|
tab_r.w = ui_w(20, 20) / 3;
|
|
|
|
if keybind_button(tab_r, "Tools", .LEVEL_TAB_TOOLS, *t_button_tab(theme, current_tab == .TOOLS)) then current_tab = .TOOLS;
|
|
tab_r.x += tab_r.w;
|
|
if keybind_button(tab_r, "Info", .LEVEL_TAB_INFO, *t_button_tab(theme, current_tab == .INFO)) then current_tab = .INFO;
|
|
tab_r.x += tab_r.w;
|
|
if keybind_button(tab_r, "Tacoma", .LEVEL_TAB_TACOMA, *t_button_tab(theme, current_tab == .TACOMA)) then current_tab = .TACOMA;
|
|
r.y += tab_r.h;
|
|
|
|
curworld := get_current_world();
|
|
if current_tab == {
|
|
case .TOOLS;
|
|
draw_tools_tab(theme, r);
|
|
case .TACOMA;
|
|
draw_tacoma_tab(theme, r);
|
|
case .INFO;
|
|
if curworld.valid then autoedit(r, *curworld.world.conf, theme);
|
|
}
|
|
if current_tool_mode == .INSPECTOR || current_tool_mode == .ENTITY {
|
|
rr := GR.get_rect(ui_w(85,0), ui_h(5,0), ui_w(15, 0), ui_h(95, 0));
|
|
draw_bg_rectangle(rr, theme);
|
|
ui_add_mouse_occluder(rr);
|
|
rr.y += ui_h(1, 0);
|
|
if current_tool_mode == .ENTITY {
|
|
draw_entity_tool_panel(*rr, theme);
|
|
} else if inspector_selected {
|
|
draw_inspector_panel(*rr, theme);
|
|
} else {
|
|
rr.h = ui_h(3, 2);
|
|
GR.label(rr, "Click a slot to inspect", *t_label_left(theme));
|
|
}
|
|
} else if current_tool_mode != .VIEWER {
|
|
draw_picker(theme);
|
|
}
|
|
}
|