trueno/src/editor/level_editor.jai

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);
}
}