trueno/src/editor/level_editor.jai
2026-03-11 13:00:25 +02:00

650 lines
18 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;
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;
}
current_tool_mode : Level_Editor_Tool_Mode = .POINT;
brush_radius : int = 2;
brush_height : int = 0;
area_active : bool;
area_start_x : int;
area_start_y : int;
area_start_z : int;
line_active : bool;
line_start_x : int;
line_start_y : int;
line_start_z : int;
current_orientation_face : u8 = 0;
current_orientation_twist : u8 = 0;
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;
tacomaExposure : float = 1.0;
tacomaContrast : float = 1.0;
tacomaSaturation : float = 1.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;
if is_in_reflection_pass {
camera.position.y *= -1;
camera.target.y *= -1;
}
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 input_button_states[Key_Code.ARROW_UP] & .START {
lastInputTime = get_time();
editY = min(editY + 1, 100);
}
if input_button_states[Key_Code.ARROW_DOWN] & .START {
lastInputTime = get_time();
editY = max(editY - 1, 0);
}
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();
r := total_r;
r.h = ui_h(3,0);
#if HAS_TACOMA {
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, "Render a RDM", *theme.button_theme) {
gen_rdm(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 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 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, "Exposure", *t_label_left(theme));
r.y += r.h;
GR.slider(r, *tacomaExposure, 0.5, 3.0, 0.1, *theme.slider_theme);
r.y += r.h;
GR.label(r, "Contrast", *t_label_left(theme));
r.y += r.h;
GR.slider(r, *tacomaContrast, 0.5, 3.0, 0.1, *theme.slider_theme);
r.y += r.h;
GR.label(r, "Saturation", *t_label_left(theme));
r.y += r.h;
GR.slider(r, *tacomaSaturation, 0.5, 3.0, 0.1, *theme.slider_theme);
r.y += r.h;
} else {
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);
// Tool mode buttons
if GR.button(r, "Point", *t_button_selectable(theme, current_tool_mode == .POINT)) {
current_tool_mode = .POINT;
}
r.y += r.h;
if GR.button(r, "Brush", *t_button_selectable(theme, current_tool_mode == .BRUSH)) {
current_tool_mode = .BRUSH;
}
r.y += r.h;
if GR.button(r, "Area", *t_button_selectable(theme, current_tool_mode == .AREA)) {
current_tool_mode = .AREA;
area_active = false;
}
r.y += r.h;
if GR.button(r, "Line", *t_button_selectable(theme, current_tool_mode == .LINE)) {
current_tool_mode = .LINE;
line_active = false;
}
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;
}
// 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, "Q/E: twist R: 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.
for *group: chunk.groups {
if group.trile_name == name {
array_add(*group.instances, inst);
return;
}
}
group: Chunk_Trile_Group;
group.trile_name = sprint("%", name);
array_add(*group.instances, inst);
array_add(*chunk.groups, group);
} @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);
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);
return;
}
}
}
} @Command
tick_level_editor :: () {
#if HAS_TACOMA { rdm_bake_tick(); }
tick_level_editor_camera();
if !console_open_ignore_input {
if input_button_states[#char "Q"] & .START {
lastInputTime = get_time();
current_orientation_twist = (current_orientation_twist + 1) % 4;
}
if input_button_states[#char "E"] & .START {
lastInputTime = get_time();
current_orientation_twist = (current_orientation_twist + 3) % 4;
}
if input_button_states[#char "R"] & .START {
lastInputTime = get_time();
current_orientation_face = (current_orientation_face + 1) % 6;
}
}
ray := get_mouse_ray(*get_level_editor_camera());
hit, point := ray_plane_collision_point(ray, xx editY, 20);
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_start_x = px;
area_start_y = py;
area_start_z = pz;
} else {
apply_area(px, py, pz, false);
area_active = false;
}
}
if get_mouse_state(Key_Code.MOUSE_BUTTON_RIGHT) & .START {
if area_active then area_active = false;
else handle_tool_click(px, py, pz, true);
}
} else if current_tool_mode == .LINE {
if get_mouse_state(Key_Code.MOUSE_BUTTON_LEFT) & .START {
if !line_active {
line_active = true;
line_start_x = px;
line_start_y = py;
line_start_z = pz;
} else {
apply_line(px, py, pz, false);
line_active = false;
}
}
if get_mouse_state(Key_Code.MOUSE_BUTTON_RIGHT) & .START {
if line_active then line_active = false;
else handle_tool_click(px, py, pz, true);
}
}
}
}
create_level_editor_preview_tasks :: () {
curworld := get_current_world();
if !curworld.valid then return;
if !editor_current_trile then return;
positions: [..]Vector4;
positions.allocator = temp;
ori := cast(float) get_current_orientation();
px := trile_preview_x; py := trile_preview_y; pz := trile_preview_z;
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;
task: Rendering_Task_Trile;
task.trile = editor_current_trile.name;
task.positions = positions;
task.worldConf = *curworld.world.conf;
task.is_preview = true;
add_rendering_task(task);
}
draw_level_editor :: () {
curworld := get_current_world();
if !curworld.valid then return;
cam := get_level_editor_camera();
create_set_cam_rendering_task(cam, effective_plane_height(*curworld.world.conf));
create_world_rendering_tasks(*curworld.world, cam);
if show_trile_preview && !trile_preview_disabled {
create_level_editor_preview_tasks();
}
}
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 GR.button(tab_r, "Tools", *t_button_tab(theme, current_tab == .TOOLS)) {
current_tab = .TOOLS;
}
tab_r.x += tab_r.w;
if GR.button(tab_r, "Info", *t_button_tab(theme, current_tab == .INFO)) {
current_tab = .INFO;
}
tab_r.x += tab_r.w;
if GR.button(tab_r, "Tacoma", *t_button_tab(theme, current_tab == .TACOMA)) {
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);
}
draw_picker(theme);
}