From cba645adcfe692161176dfd3690b21313e7a1fc8 Mon Sep 17 00:00:00 2001 From: Katajisto Date: Sun, 2 Aug 2026 11:42:00 +0300 Subject: [PATCH] add entity code --- run_tests.sh | 142 ++++ src/editor/editor.jai | 1 + src/editor/gizmo.jai | 455 ++++++++++++ src/editor/level_editor.jai | 281 ++++++- src/entities.jai | 408 ++++++++++ src/input/keybinds.jai | 2 + src/load.jai | 2 +- src/logging.jai | 6 +- src/main.jai | 25 +- src/meta/entity_types.jai | 98 +++ src/meta/meta.jai | 4 + src/meta/pack.jai | 17 +- src/orientation.jai | 291 ++++++++ src/pack_hotreload.jai | 9 +- src/rendering/backend.jai | 10 + src/rendering/backend_sokol.jai | 11 + src/rendering/debug_draw.jai | 97 ++- src/rendering/pipelines.jai | 54 +- src/rendering/tasks.jai | 30 + src/resource_mirror.jai | 162 ++++ src/shaders/jai/shader_debugline.jai | 396 +++++++--- src/shaders/shader_debugline.glsl | 32 +- src/tests/exe_tests/runner.jai | 58 +- src/tests/framework.jai | 12 + src/tests/index.jai | 5 +- src/tests/world_test.jai | 97 ++- src/ui/autoedit.jai | 56 +- src/world.jai | 72 +- test_game/game.jai | 8 + .../game_core/sprites/anim.sheet.json | 702 ------------------ .../game_core/sprites/anim.sheet.png | Bin 16446 -> 0 bytes .../game_core/sprites/score.sheet.json | 267 ------- .../game_core/sprites/score.sheet.png | Bin 6514 -> 0 bytes .../resources/worlds/test_world/world.json | 13 +- test_packs/game_core.pack | Bin 1773064 -> 1773383 bytes 35 files changed, 2680 insertions(+), 1143 deletions(-) create mode 100755 run_tests.sh create mode 100644 src/editor/gizmo.jai create mode 100644 src/entities.jai create mode 100644 src/meta/entity_types.jai create mode 100644 src/orientation.jai create mode 100644 src/resource_mirror.jai create mode 100644 src/tests/framework.jai delete mode 100644 test_game/resources/game_core/sprites/anim.sheet.json delete mode 100644 test_game/resources/game_core/sprites/anim.sheet.png delete mode 100644 test_game/resources/game_core/sprites/score.sheet.json delete mode 100644 test_game/resources/game_core/sprites/score.sheet.png diff --git a/run_tests.sh b/run_tests.sh new file mode 100755 index 0000000..966a61f --- /dev/null +++ b/run_tests.sh @@ -0,0 +1,142 @@ +#!/bin/bash +# Runs every test tier and exits nonzero if any of them fail. +# +# ./run_tests.sh # everything that can run here +# ./run_tests.sh unit # compile-time tests only (no display needed) +# ./run_tests.sh engine # engine tiers only +# ./run_tests.sh game # game tiers only +# +# Tiers: +# test_engine engine unit tests compile-time #run, builds against test_game/ +# test_game game unit tests compile-time #run, builds against game/ +# test_exe_engine engine exe tests builds then runs ./first against test_game/ +# test_exe_game game exe tests builds then runs ./first against game/ +# +# The unit tiers report through the compiler's exit code. The exe tiers run the +# real app, so they need a display and are wrapped in a hard timeout. + +set -uo pipefail +cd "$(dirname "$0")" + +JAI="${JAI:-$HOME/bin/jai/bin/jai-linux}" +EXE_TIMEOUT="${EXE_TIMEOUT:-180}" # seconds per exe-test binary, kills a true freeze + +FILTER="${1:-all}" + +if [ ! -x "$JAI" ]; then + echo "error: Jai compiler not found at '$JAI' (override with JAI=/path/to/jai)" >&2 + exit 1 +fi + +# The game/ directory is not in the repo, so the game tiers are skipped rather +# than failed on a checkout that does not have it. +HAVE_GAME=0 +[ -f game/game.jai ] && HAVE_GAME=1 + +# Exe tests open a real window. Prefer a live display, fall back to Xvfb. +XVFB=() +NO_DISPLAY=0 +if [ -z "${DISPLAY:-}" ]; then + if command -v xvfb-run >/dev/null 2>&1; then + XVFB=(xvfb-run -a) + else + NO_DISPLAY=1 + fi +fi + +PASSED=(); FAILED=(); SKIPPED=() + +want() { + case "$FILTER" in + all) return 0 ;; + unit) [[ "$1" != *exe* ]] ;; + engine) [[ "$1" == *engine* ]] ;; + game) [[ "$1" == *game* && "$1" != *engine* ]] ;; + *) echo "error: unknown filter '$FILTER' (use all|unit|engine|game)" >&2; exit 1 ;; + esac +} + +run_unit_tier() { + local flag="$1" + echo "" + echo "==============================================================" + echo " $flag (compile-time)" + echo "==============================================================" + if "$JAI" first.jai - "$flag"; then + PASSED+=("$flag") + else + FAILED+=("$flag") + fi +} + +run_exe_tier() { + local flag="$1" + echo "" + echo "==============================================================" + echo " $flag (builds, then runs the app)" + echo "==============================================================" + + if ! "$JAI" first.jai - "$flag"; then + echo "[$flag] build failed" + FAILED+=("$flag (build)") + return + fi + + timeout --foreground "$EXE_TIMEOUT" "${XVFB[@]}" ./first + local status=$? + if [ $status -eq 0 ]; then + PASSED+=("$flag") + elif [ $status -eq 124 ]; then + echo "[$flag] hung and was killed after ${EXE_TIMEOUT}s" + FAILED+=("$flag (hung)") + else + echo "[$flag] exited $status" + FAILED+=("$flag") + fi +} + +for flag in test_engine test_game; do + want "$flag" || continue + if [ "$flag" = "test_game" ] && [ "$HAVE_GAME" -eq 0 ]; then + SKIPPED+=("$flag (no game/ directory)") + continue + fi + run_unit_tier "$flag" +done + +for flag in test_exe_engine test_exe_game; do + want "$flag" || continue + if [ "$NO_DISPLAY" -eq 1 ]; then + SKIPPED+=("$flag (no DISPLAY and no xvfb-run)") + continue + fi + if [ "$flag" = "test_exe_game" ] && [ "$HAVE_GAME" -eq 0 ]; then + SKIPPED+=("$flag (no game/ directory)") + continue + fi + run_exe_tier "$flag" +done + +echo "" +echo "==============================================================" +echo " Summary" +echo "==============================================================" +for t in ${PASSED[@]+"${PASSED[@]}"}; do echo " PASS $t"; done +for t in ${SKIPPED[@]+"${SKIPPED[@]}"}; do echo " SKIP $t"; done +for t in ${FAILED[@]+"${FAILED[@]}"}; do echo " FAIL $t"; done + +if [ ${#FAILED[@]} -ne 0 ]; then + echo "" + echo "${#FAILED[@]} tier(s) failed." + exit 1 +fi + +# A run where everything was skipped is not a pass. +if [ ${#PASSED[@]} -eq 0 ]; then + echo "" + echo "Nothing ran." + exit 1 +fi + +echo "" +echo "All ${#PASSED[@]} tier(s) passed." diff --git a/src/editor/editor.jai b/src/editor/editor.jai index 7fd7068..9a289e5 100644 --- a/src/editor/editor.jai +++ b/src/editor/editor.jai @@ -2,6 +2,7 @@ #load "iprof.jai"; #load "trile_thumbnails.jai"; #load "picker.jai"; + #load "gizmo.jai"; #load "trile_editor.jai"; #load "level_editor.jai"; #load "particle_editor.jai"; diff --git a/src/editor/gizmo.jai b/src/editor/gizmo.jai new file mode 100644 index 0000000..3636096 --- /dev/null +++ b/src/editor/gizmo.jai @@ -0,0 +1,455 @@ +// Blender-style gizmos: a translate gizmo with three RGB axis arrows plus three +// plane handles for two-axis drags, and a rotate gizmo with three RGB rings. +// Drawn with depth-ignoring overlay lines so they stay visible through terrain, +// and picked in world space so the pick thresholds scale with the gizmo itself. + +Gizmo_Handle :: enum { + NONE; + AXIS_X; + AXIS_Y; + AXIS_Z; + PLANE_YZ; // normal +X + PLANE_XZ; // normal +Y + PLANE_XY; // normal +Z + RING_X; // turns about +X + RING_Y; + RING_Z; +} + +Gizmo_State :: struct { + hover : Gizmo_Handle; + active : Gizmo_Handle; + + start_position : Vector3; // object position when the drag began + start_t : float; // axis drags: parameter along the axis at grab time + start_hit : Vector3; // plane drags: world point grabbed + start_angle : float; // ring drags: angle around the ring at grab time + applied_steps : int; // ring drags: quarter turns already handed to the caller +} + +// Gizmo length as a fraction of the camera distance, so it keeps a constant +// on-screen size. Everything else is expressed in units of that length. +GIZMO_SCALE :: 0.14; +GIZMO_PLANE_INNER :: 0.22; +GIZMO_PLANE_OUTER :: 0.52; +GIZMO_PICK_RADIUS :: 0.08; +// Below this |dot(plane normal, view dir)| the plane handle is edge-on: too thin +// to aim at, so it is neither drawn nor picked. +GIZMO_PLANE_MIN_FACING :: 0.15; +// Ring radius, and how far off it a click can land, both in units of gizmo length. +GIZMO_RING_RADIUS :: 1.0; +GIZMO_RING_PICK :: 0.14; +GIZMO_RING_SEGMENTS :: 48; +// Line widths, in pixels. +GIZMO_LINE_WIDTH :: 3.0; +GIZMO_FILL_WIDTH :: 2.0; + +// Runs one frame of the gizmo: hover, click-to-grab, drag, release, and drawing. +// Returns the dragged-to position; the caller owns snapping and writing it back. +gizmo_translate :: (using state: *Gizmo_State, position: Vector3, cam: Camera, ray: Ray) -> (position: Vector3, changed: bool) { + len := max(length(cam.position - position) * GIZMO_SCALE, 0.001); + pos := position; + changed := false; + + mouse := get_mouse_state(Key_Code.MOUSE_BUTTON_LEFT); + pressed := (mouse & .START) != .NONE; + held := (mouse & .DOWN) != .NONE; + + if active == .NONE { + hover = gizmo_pick(position, len, cam, ray); + if hover != .NONE && pressed { + start_position = position; + if gizmo_is_plane(hover) { + ok, point := gizmo_plane_hit(position, hover, ray); + if ok { + start_hit = point; + active = hover; + } + } else { + t, _, _ := gizmo_closest_axis_t(ray, position, gizmo_axis(hover)); + start_t = t; + active = hover; + } + } + } else if !held { + active = .NONE; + } else { + if gizmo_is_plane(active) { + ok, point := gizmo_plane_hit(start_position, active, ray); + if ok { + pos = start_position + (point - start_hit); + changed = true; + } + } else { + axis := gizmo_axis(active); + t, _, _ := gizmo_closest_axis_t(ray, start_position, axis); + pos = start_position + axis * (t - start_t); + changed = true; + } + } + + // Drawn at the object's own position rather than the dragged-to one: the + // caller may snap what we hand back, and the gizmo has to sit on the object. + gizmo_draw(position, len, cam, ifx active != .NONE then active else hover); + return pos, changed; +} + +// One frame of the rotate gizmo. Entities only hold the 24 axis-aligned cube +// rotations, so a drag reports whole quarter turns: the returned basis is the +// (u, v, n) of the ring being dragged and 'steps' is how many turns of u toward +// v to apply since the last frame, which is usually zero. +gizmo_rotate :: (using state: *Gizmo_State, position: Vector3, cam: Camera, ray: Ray) -> (u: Vector3, v: Vector3, n: Vector3, steps: int) { + len := max(length(cam.position - position) * GIZMO_SCALE, 0.001); + + mouse := get_mouse_state(Key_Code.MOUSE_BUTTON_LEFT); + pressed := (mouse & .START) != .NONE; + held := (mouse & .DOWN) != .NONE; + + steps := 0; + + if active == .NONE { + hover = gizmo_pick_ring(position, len, cam, ray); + if hover != .NONE && pressed { + ok, angle := gizmo_ring_angle(position, hover, ray); + if ok { + start_position = position; + start_angle = angle; + applied_steps = 0; + active = hover; + } + } + } else if !held { + active = .NONE; + } else { + ok, angle := gizmo_ring_angle(start_position, active, ray); + if ok { + // Wrap into (-180, 180] so passing the seam doesn't spin the object. + delta := angle - start_angle; + while delta > PI delta -= 2 * PI; + while delta < -PI delta += 2 * PI; + total := cast(int) floor(delta / (PI * 0.5) + 0.5); + steps = total - applied_steps; + applied_steps = total; + } + } + + gizmo_draw_rings(position, len, cam, ifx active != .NONE then active else hover); + + handle := ifx active != .NONE then active else Gizmo_Handle.RING_Y; + u, v := gizmo_plane_axes(handle); + return u, v, gizmo_axis(handle), steps; +} + +gizmo_axis :: (h: Gizmo_Handle) -> Vector3 { + if h == { + case .AXIS_X; #through; + case .PLANE_YZ; #through; + case .RING_X; return .{1, 0, 0}; + case .AXIS_Y; #through; + case .PLANE_XZ; #through; + case .RING_Y; return .{0, 1, 0}; + case; return .{0, 0, 1}; + } +} + +gizmo_is_plane :: (h: Gizmo_Handle) -> bool { + return h == .PLANE_YZ || h == .PLANE_XZ || h == .PLANE_XY; +} + +gizmo_is_ring :: (h: Gizmo_Handle) -> bool { + return h == .RING_X || h == .RING_Y || h == .RING_Z; +} + +// The two in-plane axes of a plane or ring handle; gizmo_axis gives the third +// (the plane normal, or the axis the ring turns about). +gizmo_plane_axes :: (h: Gizmo_Handle) -> (u: Vector3, v: Vector3) { + if h == { + case .PLANE_YZ; #through; + case .RING_X; return .{0, 1, 0}, .{0, 0, 1}; + case .PLANE_XZ; #through; + case .RING_Y; return .{1, 0, 0}, .{0, 0, 1}; + case; return .{1, 0, 0}, .{0, 1, 0}; + } +} + +// Handles are colored by their axis; plane handles by the axis they are +// perpendicular to, which is what gizmo_axis returns for them. +gizmo_color :: (h: Gizmo_Handle, highlighted: bool) -> Vector4 { + if highlighted then return .{1.0, 0.85, 0.2, 1.0}; + axis := gizmo_axis(h); + if axis.x != 0 then return .{0.95, 0.28, 0.30, 1.0}; + if axis.y != 0 then return .{0.40, 0.90, 0.32, 1.0}; + return .{0.30, 0.50, 0.98, 1.0}; +} + +#scope_file + +// Parameter along the line (origin + axis*t) of the point closest to the ray, +// plus that point and the distance between the two lines at closest approach. +gizmo_closest_axis_t :: (ray: Ray, origin: Vector3, axis: Vector3) -> (t: float, point: Vector3, dist: float) { + r := origin - ray.origin; + a := dot(axis, axis); + b := dot(axis, ray.direction); + e := dot(ray.direction, ray.direction); + c := dot(axis, r); + f := dot(ray.direction, r); + + denom := a*e - b*b; + if abs(denom) < 0.00001 then return 0, origin, 99999; // ray parallel to the axis + + t := (b*f - c*e) / denom; + s := (a*f - c*b) / denom; + pa := origin + axis * t; + pb := ray.origin + ray.direction * s; + return t, pa, length(pa - pb); +} + +gizmo_plane_hit :: (origin: Vector3, h: Gizmo_Handle, ray: Ray) -> (bool, Vector3) { + n := gizmo_axis(h); + denom := dot(n, ray.direction); + if abs(denom) < 0.0001 then return false, .{}; + t := dot(n, origin - ray.origin) / denom; + if t < 0 then return false, .{}; // plane is behind the camera + return true, ray.origin + ray.direction * t; +} + +// Which way each plane handle is offset from the origin: toward the camera, so +// the handles always sit in the octant facing the viewer. +gizmo_plane_signs :: (origin: Vector3, h: Gizmo_Handle, cam: Camera) -> (su: float, sv: float) { + u, v := gizmo_plane_axes(h); + to_cam := cam.position - origin; + su := ifx dot(u, to_cam) >= 0 then cast(float)1 else cast(float)-1; + sv := ifx dot(v, to_cam) >= 0 then cast(float)1 else cast(float)-1; + return su, sv; +} + +gizmo_pick :: (origin: Vector3, len: float, cam: Camera, ray: Ray) -> Gizmo_Handle { + // Planes first: they sit closer to the origin and never overlap the arrows, + // but grabbing a plane is the more common intent when they are near. + for h: Gizmo_Handle.[.PLANE_YZ, .PLANE_XZ, .PLANE_XY] { + n := gizmo_axis(h); + if abs(dot(n, normalize(cam.position - origin))) < GIZMO_PLANE_MIN_FACING then continue; + ok, point := gizmo_plane_hit(origin, h, ray); + if !ok then continue; + u, v := gizmo_plane_axes(h); + su, sv := gizmo_plane_signs(origin, h, cam); + d := point - origin; + cu := dot(d, u) * su; + cv := dot(d, v) * sv; + if cu >= GIZMO_PLANE_INNER * len && cu <= GIZMO_PLANE_OUTER * len + && cv >= GIZMO_PLANE_INNER * len && cv <= GIZMO_PLANE_OUTER * len { + return h; + } + } + + best := Gizmo_Handle.NONE; + best_dist := GIZMO_PICK_RADIUS * len; + for h: Gizmo_Handle.[.AXIS_X, .AXIS_Y, .AXIS_Z] { + t, _, dist := gizmo_closest_axis_t(ray, origin, gizmo_axis(h)); + if t < 0 || t > len then continue; + if dist < best_dist { + best_dist = dist; + best = h; + } + } + return best; +} + +gizmo_draw :: (origin: Vector3, len: float, cam: Camera, highlighted: Gizmo_Handle) { + for h: Gizmo_Handle.[.PLANE_YZ, .PLANE_XZ, .PLANE_XY] { + n := gizmo_axis(h); + if abs(dot(n, normalize(cam.position - origin))) < GIZMO_PLANE_MIN_FACING then continue; + u, v := gizmo_plane_axes(h); + su, sv := gizmo_plane_signs(origin, h, cam); + col := gizmo_color(h, h == highlighted); + gizmo_draw_plane(origin, u * su, v * sv, GIZMO_PLANE_INNER * len, GIZMO_PLANE_OUTER * len, col, h == highlighted); + } + + for h: Gizmo_Handle.[.AXIS_X, .AXIS_Y, .AXIS_Z] { + gizmo_draw_arrow(origin, gizmo_axis(h) * len, gizmo_color(h, h == highlighted)); + } +} + +gizmo_draw_arrow :: (origin: Vector3, vec: Vector3, col: Vector4) { + tip := origin + vec; + shaft := normalize(vec); + debug_line_overlay(origin, tip, col, GIZMO_LINE_WIDTH); + + head := length(vec) * 0.18; + back := tip - shaft * head; + perp : Vector3; + if abs(shaft.x) < 0.9 { + perp = normalize(cross(shaft, .{1, 0, 0})); + } else { + perp = normalize(cross(shaft, .{0, 1, 0})); + } + perp2 := cross(shaft, perp); + w1 := perp * (head * 0.4); + w2 := perp2 * (head * 0.4); + // Four fins plus a ring, so the head reads as a cone from any angle. + debug_line_overlay(back + w1, tip, col, GIZMO_LINE_WIDTH); + debug_line_overlay(back - w1, tip, col, GIZMO_LINE_WIDTH); + debug_line_overlay(back + w2, tip, col, GIZMO_LINE_WIDTH); + debug_line_overlay(back - w2, tip, col, GIZMO_LINE_WIDTH); + debug_line_overlay(back + w1, back + w2, col, GIZMO_LINE_WIDTH); + debug_line_overlay(back + w2, back - w1, col, GIZMO_LINE_WIDTH); + debug_line_overlay(back - w1, back - w2, col, GIZMO_LINE_WIDTH); + debug_line_overlay(back - w2, back + w1, col, GIZMO_LINE_WIDTH); +} + +// A square in the (u, v) plane between inner and outer, outlined at full alpha +// and hatched at low alpha so it reads as a translucent pane. +gizmo_draw_plane :: (origin: Vector3, u: Vector3, v: Vector3, inner: float, outer: float, col: Vector4, filled: bool) { + p00 := origin + u * inner + v * inner; + p10 := origin + u * outer + v * inner; + p11 := origin + u * outer + v * outer; + p01 := origin + u * inner + v * outer; + debug_line_overlay(p00, p10, col, GIZMO_LINE_WIDTH); + debug_line_overlay(p10, p11, col, GIZMO_LINE_WIDTH); + debug_line_overlay(p11, p01, col, GIZMO_LINE_WIDTH); + debug_line_overlay(p01, p00, col, GIZMO_LINE_WIDTH); + + fill := col; + fill.w = ifx filled then 0.55 else 0.18; + // Enough hatch lines that the pane reads as filled rather than striped. + HATCH :: 14; + for i: 1..HATCH { + f := cast(float)i / cast(float)(HATCH + 1); + c := inner + (outer - inner) * f; + debug_line_overlay(origin + u * c + v * inner, origin + u * c + v * outer, fill, GIZMO_FILL_WIDTH); + } +} + +// Angle of the mouse ray's hit on the ring's plane, measured from u toward v. +gizmo_ring_angle :: (origin: Vector3, h: Gizmo_Handle, ray: Ray) -> (ok: bool, angle: float) { + ok, point := gizmo_plane_hit(origin, h, ray); + if !ok then return false, 0; + u, v := gizmo_plane_axes(h); + d := point - origin; + return true, atan2(dot(d, v), dot(d, u)); +} + +gizmo_pick_ring :: (origin: Vector3, len: float, cam: Camera, ray: Ray) -> Gizmo_Handle { + best := Gizmo_Handle.NONE; + best_dist := GIZMO_RING_PICK * len; + for h: Gizmo_Handle.[.RING_X, .RING_Y, .RING_Z] { + if !gizmo_ring_faces_camera(origin, h, cam) then continue; + ok, point := gizmo_plane_hit(origin, h, ray); + if !ok then continue; + // Distance from the ring itself, not from its centre. + off := abs(length(point - origin) - GIZMO_RING_RADIUS * len); + if off < best_dist { + best_dist = off; + best = h; + } + } + return best; +} + +// An edge-on ring is a line on screen: impossible to aim at, and its plane +// intersection shoots off to infinity, so skip it. +gizmo_ring_faces_camera :: (origin: Vector3, h: Gizmo_Handle, cam: Camera) -> bool { + return abs(dot(gizmo_axis(h), normalize(cam.position - origin))) >= GIZMO_PLANE_MIN_FACING; +} + +gizmo_draw_rings :: (origin: Vector3, len: float, cam: Camera, highlighted: Gizmo_Handle) { + for h: Gizmo_Handle.[.RING_X, .RING_Y, .RING_Z] { + is_hot := h == highlighted; + if !gizmo_ring_faces_camera(origin, h, cam) && !is_hot then continue; + u, v := gizmo_plane_axes(h); + col := gizmo_color(h, is_hot); + r := GIZMO_RING_RADIUS * len; + prev := origin + u * r; + for i: 1..GIZMO_RING_SEGMENTS { + a := (cast(float)i / cast(float)GIZMO_RING_SEGMENTS) * 2 * PI; + next := origin + u * (cos(a) * r) + v * (sin(a) * r); + debug_line_overlay(prev, next, col, GIZMO_LINE_WIDTH); + prev = next; + } + } +} + +#if FLAG_TEST_ENGINE { + gizmo_test_cam :: () -> Camera { + // Off the +X+Y+Z octant, so every plane handle faces the viewer and its + // signs are all positive. + cam : Camera; + cam.position = .{5, 5, 5}; + cam.target = .{0, 0, 0}; + return cam; + } + + test_gizmo_closest_axis :: () { + s := begin_suite("gizmo closest point on axis"); + // Vertical ray dropping onto the +X axis at x = 0.5. + ray := Ray.{ origin = .{0.5, 3, 0}, direction = .{0, -1, 0} }; + t, point, dist := gizmo_closest_axis_t(ray, .{0, 0, 0}, .{1, 0, 0}); + check(*s, "t is the distance along the axis", abs(t - 0.5) < 0.001); + check(*s, "closest point sits on the axis", abs(point.x - 0.5) < 0.001 && abs(point.y) < 0.001 && abs(point.z) < 0.001); + check(*s, "intersecting lines have 0 distance", dist < 0.001); + + // A ray offset in Z never meets the axis; the gap is that offset. + off := Ray.{ origin = .{0.5, 3, 0.25}, direction = .{0, -1, 0} }; + _, _, off_dist := gizmo_closest_axis_t(off, .{0, 0, 0}, .{1, 0, 0}); + check(*s, "offset ray reports its offset as distance", abs(off_dist - 0.25) < 0.001); + + // Parallel lines have no unique closest point. + par := Ray.{ origin = .{0, 3, 0}, direction = .{1, 0, 0} }; + _, _, par_dist := gizmo_closest_axis_t(par, .{0, 0, 0}, .{1, 0, 0}); + check(*s, "parallel ray is rejected", par_dist > 1000); + end_suite(s); + } + + test_gizmo_pick :: () { + s := begin_suite("gizmo handle picking"); + cam := gizmo_test_cam(); + len : float : 1.0; + + // Straight down onto the middle of the +X arrow. + axis_ray := Ray.{ origin = .{0.5, 3, 0}, direction = .{0, -1, 0} }; + check(*s, "aiming at the X arrow picks AXIS_X", gizmo_pick(.{0,0,0}, len, cam, axis_ray) == .AXIS_X); + + // Down onto the XZ pane, which spans 0.22..0.52 on both of its axes. + plane_ray := Ray.{ origin = .{0.35, 3, 0.35}, direction = .{0, -1, 0} }; + check(*s, "aiming at the XZ pane picks PLANE_XZ", gizmo_pick(.{0,0,0}, len, cam, plane_ray) == .PLANE_XZ); + + // Inside the pane's plane but short of its inner edge: no handle there. + gap_ray := Ray.{ origin = .{0.1, 3, 0.1}, direction = .{0, -1, 0} }; + check(*s, "the gap inside the panes picks nothing", gizmo_pick(.{0,0,0}, len, cam, gap_ray) == .NONE); + + // Past the end of every arrow. + miss_ray := Ray.{ origin = .{2, 3, 2}, direction = .{0, -1, 0} }; + check(*s, "aiming past the gizmo picks nothing", gizmo_pick(.{0,0,0}, len, cam, miss_ray) == .NONE); + + // Picking follows the gizmo, not the world origin. + moved := Vector3.{10, 4, -7}; + moved_ray := Ray.{ origin = moved + Vector3.{0.5, 3, 0}, direction = .{0, -1, 0} }; + check(*s, "picking is relative to the gizmo origin", gizmo_pick(moved, len, cam, moved_ray) == .AXIS_X); + end_suite(s); + } + + test_gizmo_plane_hit :: () { + s := begin_suite("gizmo plane intersection"); + // PLANE_XZ has a +Y normal, so it is the horizontal plane through the origin. + ray := Ray.{ origin = .{1, 4, 2}, direction = .{0, -1, 0} }; + ok, point := gizmo_plane_hit(.{0, 0, 0}, .PLANE_XZ, ray); + check(*s, "downward ray hits the XZ pane's plane", ok); + check(*s, "hit lands at the ray's XZ", abs(point.x - 1) < 0.001 && abs(point.z - 2) < 0.001); + check(*s, "hit lands on the plane", abs(point.y) < 0.001); + + // The plane passes through the gizmo origin, not the world origin. + shifted, sp := gizmo_plane_hit(.{0, 2.5, 0}, .PLANE_XZ, ray); + check(*s, "plane follows the gizmo origin", shifted && abs(sp.y - 2.5) < 0.001); + + away := Ray.{ origin = .{1, 4, 2}, direction = .{0, 1, 0} }; + no_hit, _ := gizmo_plane_hit(.{0, 0, 0}, .PLANE_XZ, away); + check(*s, "ray pointing away misses", !no_hit); + end_suite(s); + } + + #run { + test_gizmo_closest_axis(); + test_gizmo_pick(); + test_gizmo_plane_hit(); + } +} diff --git a/src/editor/level_editor.jai b/src/editor/level_editor.jai index d0de755..bbf626a 100644 --- a/src/editor/level_editor.jai +++ b/src/editor/level_editor.jai @@ -29,6 +29,7 @@ Level_Editor_Tool_Mode :: enum { LINE; INSPECTOR; VIEWER; + ENTITY; } current_tool_mode : Level_Editor_Tool_Mode = .POINT; @@ -59,6 +60,21 @@ inspector_rdm_roughness : int = 0; 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 { @@ -333,6 +349,8 @@ draw_tools_tab :: (theme: *GR.Overall_Theme, total_r: GR.Rect) { 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; @@ -370,7 +388,7 @@ draw_tools_tab :: (theme: *GR.Overall_Theme, total_r: GR.Rect) { GR.label(r, "Click start of line", *t_label_left(theme)); } r.y += r.h; - } else if current_tool_mode == .INSPECTOR { + } 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; @@ -510,6 +528,121 @@ remove_trile :: (x: s32, y: s32, z: s32) { +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(); @@ -525,6 +658,9 @@ tick_level_editor :: () { 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; @@ -538,9 +674,14 @@ tick_level_editor :: () { current_orientation_face = (current_orientation_face + 1) % 6; } - ray := get_mouse_ray(*get_level_editor_camera()); + 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; @@ -632,6 +773,12 @@ tick_level_editor :: () { 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); + } } } } @@ -642,6 +789,9 @@ create_level_editor_preview_tasks :: () { 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 ""); @@ -980,11 +1130,131 @@ draw_inspector_panel :: (r: *GR.Rect, theme: *GR.Overall_Theme) { } } +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)"); +} + add_editor_billboards :: () { 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; + task : Rendering_Task_Billboard; + task.position = e.position + ENTITY_PIVOT; + task.animation = marker; + task.frame = 0; + add_rendering_task(task); + } + anim := get_animation_from_string("game_core.ball"); if anim == null then return; @@ -1012,6 +1282,7 @@ draw_level_editor :: () { ph := effective_plane_height(*curworld.world.conf); create_set_cam_rendering_task(cam, ph); create_world_rendering_tasks(*curworld.world, cam, ph); + add_entity_render_tasks(*curworld.world); if show_trile_preview && !trile_preview_disabled { create_level_editor_preview_tasks(); } @@ -1043,12 +1314,14 @@ draw_level_editor_ui :: (theme: *GR.Overall_Theme) { case .INFO; if curworld.valid then autoedit(r, *curworld.world.conf, theme); } - if current_tool_mode == .INSPECTOR { + 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 inspector_selected { + 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); diff --git a/src/entities.jai b/src/entities.jai new file mode 100644 index 0000000..006e3b6 --- /dev/null +++ b/src/entities.jai @@ -0,0 +1,408 @@ +// Entity types are plain structs declared in game code, registered with an +// @Entity note the way console commands register with @Command. The metaprogram +// (src/meta/entity_types.jai) collects them into ENTITY_TYPES, and spawning, +// dispatch, editor UI and saving all come from that. Marking a struct @Entity +// without embedding Entity, or embedding Entity without the note, is an error. +// +// Foo :: struct { +// #as using base : Entity; +// base.type = Foo; +// PARTS :: Entity_Part.[...]; // visuals, may be empty +// EDITOR_MARKER :: "pack.animation"; // optional, editor-only billboard +// } @Entity +// entity_tick :: (using foo: *Foo, dt: float) {} // optional +// entity_draw :: (foo: *Foo) {} // optional +// +// Fields of type float, s32, bool, string or Vector3 are editable and saved; +// @Slider / @Color notes pick the editor widget. +// +// Each frame the game calls tick_entities, then tick_entity_emitters, then +// add_entity_render_tasks. The level editor calls the last two only: it shows +// entities and their particles without running their game logic. + +#scope_export + +Entity :: struct { + type : Type; + id : u32; + position : Vector3; + orientation : u8; // 0..23, see orientation.jai + emitters : [..]Particle_Emitter_Instance; // built from PARTS, not saved +} + +Entity_Part_Kind :: enum { + TRILE; + BILLBOARD; + EMITTER; +} + +Entity_Part :: struct { + kind : Entity_Part_Kind; + trile : string; // TRILE + orientation : u8; // TRILE, 0..23 + animation : string; // BILLBOARD, e.g. "game_core.ball" + emitter : string; // EMITTER, a particle definition name + offset : Vector3; +} + +Entity_Type_Info :: struct { + name : string; + type : Type; + parts : []Entity_Part; + editor_marker : string; + create : () -> *Entity; +} + +#insert #run,stallable gen_entity_type_table(); + +get_entity_type_info :: (type: Type) -> *Entity_Type_Info { + for * ENTITY_TYPE_TABLE if it.type == type return it; + return null; +} + +get_entity_parts :: (type: Type) -> []Entity_Part { + info := get_entity_type_info(type); + return ifx info then info.parts else .[]; +} + +get_entity_editor_marker :: (type: Type) -> string { + info := get_entity_type_info(type); + return ifx info then info.editor_marker else ""; +} + +entity_type_name :: (type: Type) -> string { + return (cast(*Type_Info_Struct) type).name; +} + +// Entities turn about the middle of their own cell, so a rotated entity stays +// on the block it was placed on. +ENTITY_PIVOT :: Vector3.{0.5, 0.5, 0.5}; + +entity_point :: (e: *Entity, offset: Vector3) -> Vector3 { + return e.position + ENTITY_PIVOT + rotate_by_orientation(e.orientation, offset - ENTITY_PIVOT); +} + +// A trile part fills a cell rather than being a point, so the pivot cancels out. +entity_trile_point :: (e: *Entity, offset: Vector3) -> Vector3 { + return e.position + rotate_by_orientation(e.orientation, offset); +} + +// Orientation is taken up front rather than assigned afterwards because the +// emitters are placed relative to it. +spawn_entity :: (world: *World, type_name: string, position: Vector3, orientation: u8 = 0) -> *Entity { + for ENTITY_TYPE_TABLE { + if it.name != type_name continue; + e := it.create(); + e.position = position; + e.orientation = orientation; + e.id = world.next_entity_id; + world.next_entity_id += 1; + init_entity_emitters(e); + array_add(*world.entities, e); + return e; + } + log_error("Unknown entity type: %", type_name); + return null; +} + +get_first_entity :: (world: *World, type: Type) -> *Entity { + for e: world.entities if e.type == type return e; + return null; +} + +remove_entity :: (world: *World, e: *Entity) { + for other, i: world.entities { + if other != e continue; + array_ordered_remove_by_index(*world.entities, i); + free_entity(e); + return; + } +} + +free_entity :: (e: *Entity) { + array_free(e.emitters); + free(e); +} + +free_all_entities :: (world: *World) { + for e: world.entities free_entity(e); + array_reset(*world.entities); +} + +// Concrete per-type overloads in game code take precedence over these. +entity_tick :: (e: *$T/Entity, dt: float) {} +entity_draw :: (e: *$T/Entity) { draw_entity_parts(e); } + +tick_entities :: (world: *World, dt: float) { + for e: world.entities entity_dispatch_tick(e, dt); +} + +entity_dispatch_tick :: (e: *Entity, dt: float) { + #insert #run,stallable gen_entity_dispatch("entity_tick(cast(*%)e, dt)"); +} + +entity_dispatch_draw :: (e: *Entity) { + #insert #run,stallable gen_entity_dispatch("entity_draw(cast(*%)e)"); +} + +// Dispatches a call on the entity's concrete type. '%' in 'call' becomes the type +// name. Also used by the editor to run autoedit on an entity's own fields. +gen_entity_dispatch :: (call: string) -> string { + builder : String_Builder; + for ENTITY_TYPES { + name := (cast(*Type_Info_Struct) it).name; + print_to_builder(*builder, "if e.type == % { %; return; }\n", name, tprint(call, name)); + } + return builder_to_string(*builder); +} + +init_entity_emitters :: (e: *Entity) { + for part: get_entity_parts(e.type) { + if part.kind != .EMITTER continue; + inst : Particle_Emitter_Instance; + inst.definition_name = part.emitter; + inst.definition = get_emitter_def(part.emitter); + inst.offset = part.offset; + inst.position = entity_point(e, part.offset); + inst.active = true; + array_add(*e.emitters, inst); + } +} + +tick_entity_emitters :: (world: *World, dt: float) { + for e: world.entities { + for *inst: e.emitters { + if inst.definition == null { + inst.definition = get_emitter_def(inst.definition_name); + if inst.definition == null continue; + } + inst.position = entity_point(e, inst.offset); + tick_emitter_instance(inst, dt); + } + } +} + +// Once per frame, from the game and from the editor. Trile draws are batched per +// (trile, chunk) and flushed as TRILE_DYNAMIC tasks at the end. +add_entity_render_tasks :: (world: *World) { + array_reset_keeping_memory(*entity_trile_batches); + for e: world.entities entity_dispatch_draw(e); + for *batch: entity_trile_batches { + task : Rendering_Task_Trile_Dynamic; + task.trile = batch.trile; + task.positions = batch.positions; + task.chunk_key = batch.chunk_key; + task.worldConf = *world.conf; + add_rendering_task(task); + } +} + +entity_draw_trile :: (e: *Entity, trile: string, offset: Vector3, orientation: u8 = 0) { + pos := entity_trile_point(e, offset); + ori := compose_orientations(e.orientation, orientation); + key := world_to_chunk_coord(cast(s32) floor(pos.x), cast(s32) floor(pos.y), cast(s32) floor(pos.z)); + instance := Vector4.{pos.x, pos.y, pos.z, cast(float) ori}; + + for *batch: entity_trile_batches { + if batch.trile == trile && batch.chunk_key == key { + array_add(*batch.positions, instance); + return; + } + } + batch : Entity_Trile_Batch; + batch.trile = trile; + batch.chunk_key = key; + batch.positions.allocator = temp; + array_add(*batch.positions, instance); + array_add(*entity_trile_batches, batch); +} + +entity_draw_billboard :: (e: *Entity, animation: string, offset: Vector3, frame: s32 = 0) { + anim := get_animation_from_string(animation); + if anim == null return; + task : Rendering_Task_Billboard; + task.position = entity_point(e, offset); + task.animation = anim; + task.frame = frame; + add_rendering_task(task); +} + +draw_entity_parts :: (e: *Entity) { + for part: get_entity_parts(e.type) { + if part.kind == { + case .TRILE; entity_draw_trile(e, part.trile, part.offset, part.orientation); + case .BILLBOARD; entity_draw_billboard(e, part.animation, part.offset); + case .EMITTER; // tick_entity_emitters handles these + } + } +} + +// Fields are saved as name/value string pairs rather than typed JSON, so adding, +// removing or renaming one never breaks loading an older world. +Entity_Field :: struct { + name : string; + value : string; +} + +entity_fields_to_strings :: (e: *Entity) -> []Entity_Field { + result : [..]Entity_Field; + result.allocator = temp; + for member: entity_saved_members(e.type) { + ptr := (cast(*u8) e) + member.offset_in_bytes; + value : string; + if member.type == type_info(float) value = tprint("%", (cast(*float) ptr).*); + else if member.type == type_info(s32) value = tprint("%", (cast(*s32) ptr).*); + else if member.type == type_info(bool) value = tprint("%", (cast(*bool) ptr).*); + else if member.type == type_info(string) value = (cast(*string) ptr).*; + else if member.type == type_info(Vector3) { + v := (cast(*Vector3) ptr).*; + value = tprint("% % %", v.x, v.y, v.z); + } else continue; + array_add(*result, .{member.name, value}); + } + return result; +} + +entity_apply_field :: (e: *Entity, name: string, value: string) { + for member: entity_saved_members(e.type) { + if member.name != name continue; + ptr := (cast(*u8) e) + member.offset_in_bytes; + if member.type == type_info(float) { + v, ok := string_to_float(value); + if ok { (cast(*float) ptr).* = v; } + } else if member.type == type_info(s32) { + v, ok := string_to_int(value); + if ok { (cast(*s32) ptr).* = cast(s32) v; } + } else if member.type == type_info(bool) { + (cast(*bool) ptr).* = (value == "true"); + } else if member.type == type_info(string) { + (cast(*string) ptr).* = copy_string(value); + } else if member.type == type_info(Vector3) { + parts := split(value, " ",, temp); + if parts.count != 3 return; + v : Vector3; + x, okx := string_to_float(parts[0]); + y, oky := string_to_float(parts[1]); + z, okz := string_to_float(parts[2]); + if okx && oky && okz { (cast(*Vector3) ptr).* = .{x, y, z}; } + } + return; + } + log_warn("Entity type % has no field '%', skipping", entity_type_name(e.type), name); +} + +#scope_file + +#import "String"; + +// The members an entity type declares itself: constants and the Entity base are +// neither editable nor saved. +entity_saved_members :: (type: Type) -> []Type_Info_Struct_Member { + result : [..]Type_Info_Struct_Member; + result.allocator = temp; + for (cast(*Type_Info_Struct) type).members { + if it.flags & (.CONSTANT | .USING) continue; + array_add(*result, it); + } + return result; +} + +Entity_Trile_Batch :: struct { + trile : string; + chunk_key : Chunk_Key; + positions : [..]Vector4; +} + +entity_trile_batches : [..]Entity_Trile_Batch; + +#if FLAG_TEST_ENGINE { + // The two placement helpers use different pivots, and getting them the wrong + // way round only shows up as parts drifting off a rotated entity, so both are + // pinned to the invariant they exist for. + test_entity_part_placement :: () { + s := begin_suite("entity part placement"); + + e : Entity; + e.position = .{10, 3, -5}; + + // A trile part on the entity's own cell has to stay on that cell however + // the entity is turned, or rotating would walk it off the block it was + // placed on. + on_its_block := true; + for ori: 0..ORIENTATION_COUNT-1 { + e.orientation = cast(u8) ori; + if entity_trile_point(*e, .{0, 0, 0}) != e.position on_its_block = false; + } + check(*s, "a trile part on the entity's own cell never moves", on_its_block); + + // Point parts turn about the middle of that same cell, so a part sitting + // at the middle is the fixed point of all 24 rotations. + at_the_middle := true; + for ori: 0..ORIENTATION_COUNT-1 { + e.orientation = cast(u8) ori; + if entity_point(*e, ENTITY_PIVOT) != e.position + ENTITY_PIVOT at_the_middle = false; + } + check(*s, "a point part at the middle never moves", at_the_middle); + + e.orientation = 0; + check(*s, "unturned point parts sit at their authored offset", + entity_point(*e, .{1, 2, 3}) == e.position + Vector3.{1, 2, 3}); + check(*s, "unturned trile parts sit at their authored offset", + entity_trile_point(*e, .{1, 2, 3}) == e.position + Vector3.{1, 2, 3}); + + // Orientation 1 is the quarter twist about Y, which sends X to Z. + e.orientation = 1; + check(*s, "a turned entity carries its parts around with it", + entity_trile_point(*e, .{1, 0, 0}) == e.position + Vector3.{0, 0, 1}); + + end_suite(s); + } + + // Every declared type must reach the table, or it would be unspawnable and + // unsaveable with no diagnostic anywhere. + test_entity_type_table :: () { + s := begin_suite("entity type table"); + check(*s, "the table covers every declared type", ENTITY_TYPE_TABLE.count == ENTITY_TYPES.count); + + found_all := true; + for type: ENTITY_TYPES if get_entity_type_info(type) == null found_all = false; + check(*s, "every declared type is findable by type", found_all); + + names_match := true; + for info: ENTITY_TYPE_TABLE if info.name != entity_type_name(info.type) names_match = false; + check(*s, "table names match the type names saves use", names_match); + + // Entity itself is never a declared type, so it stands in for one that + // this build does not have. + check(*s, "an unknown type has no info", get_entity_type_info(Entity) == null); + check(*s, "an unknown type has no parts", get_entity_parts(Entity).count == 0); + end_suite(s); + } + + // Stallable because the type table is: ENTITY_TYPES only exists once the + // metaprogram has seen every @Entity in the program. + #run,stallable { + test_entity_part_placement(); + test_entity_type_table(); + } +} + +gen_entity_type_table :: () -> string { + builder : String_Builder; + append(*builder, "ENTITY_TYPE_TABLE :: Entity_Type_Info.[\n"); + for ENTITY_TYPES { + ti := cast(*Type_Info_Struct) it; + marker := "\"\""; + for member: ti.members { + if member.name == "EDITOR_MARKER" && (member.flags & .CONSTANT) { + marker = tprint("%.EDITOR_MARKER", ti.name); + break; + } + } + print_to_builder(*builder, + " .{name = \"%\", type = %, parts = %.PARTS, editor_marker = %, create = () -> *Entity { return New(%); }},\n", + ti.name, ti.name, ti.name, marker, ti.name); + } + append(*builder, "];\n"); + return builder_to_string(*builder); +} diff --git a/src/input/keybinds.jai b/src/input/keybinds.jai index 2061043..3d682ac 100644 --- a/src/input/keybinds.jai +++ b/src/input/keybinds.jai @@ -28,6 +28,7 @@ Editor_Action :: enum { LEVEL_TOOL_LINE; LEVEL_TOOL_INSPECTOR; LEVEL_TOOL_VIEWER; + LEVEL_TOOL_ENTITY; // Level editor — tabs LEVEL_TAB_TOOLS; LEVEL_TAB_INFO; @@ -180,6 +181,7 @@ set_default_bindings :: () { set(.LEVEL_TOOL_LINE, cast(Key_Code) #char "4"); set(.LEVEL_TOOL_INSPECTOR, cast(Key_Code) #char "5"); set(.LEVEL_TOOL_VIEWER, cast(Key_Code) #char "6"); + set(.LEVEL_TOOL_ENTITY, cast(Key_Code) #char "7"); set(.LEVEL_TAB_TOOLS, cast(Key_Code) #char "T"); set(.LEVEL_TAB_INFO, cast(Key_Code) #char "I"); set(.LEVEL_TAB_TACOMA, cast(Key_Code) #char "X"); diff --git a/src/load.jai b/src/load.jai index b15c950..df6b7a7 100644 --- a/src/load.jai +++ b/src/load.jai @@ -1,4 +1,4 @@ -#if FLAG_TEST_EXE_ENGINE { +#if FLAG_USE_TEST_GAME { PACK_DIR :: "./test_packs"; GAME_RESOURCES_DIR :: "./test_game/resources"; } else { diff --git a/src/logging.jai b/src/logging.jai index f5178c8..1bcff01 100644 --- a/src/logging.jai +++ b/src/logging.jai @@ -18,8 +18,10 @@ _emit :: (level: Log_Level, message: string) { else "[INFO] "; // We need to protect the logger from custom allocators since we don't - // want those meddling with our log row allocation! - push_allocator(default_allocator); + // want those meddling with our log row allocation! Before the engine has + // set one up — at compile time, in a #run — there is nothing to protect + // against and nothing to allocate from, so the current one has to do. + push_allocator(ifx default_allocator.proc then default_allocator else context.allocator); line := copy_string(tprint("%1%2", prefix, message)); print("%\n", line); console_add_output_line(line); diff --git a/src/main.jai b/src/main.jai index e801ce6..52aaff8 100644 --- a/src/main.jai +++ b/src/main.jai @@ -15,6 +15,17 @@ String :: #import "String"; Jaison :: #import "Jaison"; stbi :: #import "stb_image"; +FLAG_ANY_TEST :: FLAG_TEST_ENGINE || FLAG_TEST_EXE_ENGINE || FLAG_TEST_GAME || FLAG_TEST_EXE_GAME; +FLAG_ANY_EXE_TEST :: FLAG_TEST_EXE_ENGINE || FLAG_TEST_EXE_GAME; + +// The engine's own tests build against test_game/, so they still compile on a +// checkout that does not have the game/ directory. Game tests build against the +// real game, which is the whole point of them. +FLAG_USE_TEST_GAME :: FLAG_TEST_ENGINE || FLAG_TEST_EXE_ENGINE; + +// The framework (suites, checks, the exe-test command list) is available under +// every test flag; the engine's own test cases are not. +#if FLAG_ANY_TEST { #load "tests/framework.jai"; } #if (FLAG_TEST_ENGINE || FLAG_TEST_EXE_ENGINE) { #load "tests/index.jai"; } #load "logging.jai"; @@ -32,6 +43,8 @@ stbi :: #import "stb_image"; #load "ray.jai"; #load "profiling.jai"; #load "particles/particles.jai"; +#load "orientation.jai"; +#load "entities.jai"; #load "world.jai"; #load "utils.jai"; #load "audio/audio.jai"; @@ -40,10 +53,10 @@ stbi :: #import "stb_image"; #load "loading_screen.jai"; #load "ui/demo.jai"; -#if !FLAG_TEST_EXE_ENGINE { - #load "../game/game.jai"; -} else { +#if FLAG_USE_TEST_GAME { #load "../test_game/game.jai"; +} else { + #load "../game/game.jai"; } last_frame_time : float64; // timestamp of the last frame @@ -139,6 +152,10 @@ init :: () { #if FLAG_TEST_EXE_ENGINE { engine_exe_tests_add(); } + #if FLAG_TEST_EXE_GAME { + // Fixed-name hook, same convention as game_init / game_tick. + game_exe_tests_add(); + } } init_after_core :: () { @@ -225,7 +242,7 @@ frame :: () { add_frame_profiling_point("After loading logic"); - #if FLAG_TEST_EXE_ENGINE { + #if FLAG_ANY_EXE_TEST { run_exe_tests(); } diff --git a/src/meta/entity_types.jai b/src/meta/entity_types.jai new file mode 100644 index 0000000..382201e --- /dev/null +++ b/src/meta/entity_types.jai @@ -0,0 +1,98 @@ +/* + Entity types register themselves with an @Entity note on the struct, the same + way console commands register with @Command. The generators in entities.jai + all read one list, ENTITY_TYPES, which is built here and injected once the + rest of the program has been typechecked — that is why those generators are + written as #run,stallable. +*/ + +#import "Sort"; + +entity_type_names : [..]string; +entity_types_emitted := false; + +add_entity_types :: (message: *Message, w: *Workspace) { + if message.kind == .PHASE { + phase := cast(*Message_Phase) message; + if phase.phase == .TYPECHECKED_ALL_WE_CAN && !entity_types_emitted { + entity_types_emitted = true; + emit_entity_types(w); + } + return; + } + + if message.kind != .TYPECHECKED then return; + code := cast(*Message_Typechecked) message; + + for code.declarations { + decl := it.expression; + if !decl.expression || decl.expression.kind != .STRUCT then continue; + if !is_main_program(decl) then continue; + + noted := false; + for note: decl.notes if note.text == "Entity" noted = true; + embeds := struct_embeds_entity(cast(*Code_Struct) decl.expression); + + // Without this the note is as easy to forget as the hand-written list it + // replaced, and forgetting it is silent: the type compiles fine and is + // simply invisible to spawning, the editor and saving. + if embeds && !noted { + report(decl, sprint("Struct % embeds Entity but is not marked @Entity, so nothing can spawn, edit or save it.", decl.name)); + continue; + } + if !noted then continue; + + if !embeds { + report(decl, sprint("Struct % is marked @Entity but has no '#as using base : Entity;'.", decl.name)); + continue; + } + if decl.flags & .SCOPE_FILE { + report(decl, sprint("Entity type % is file scoped, that is not okay. The generated type table has to name it.", decl.name)); + continue; + } + + array_add(*entity_type_names, sprint("%", decl.name)); + } +} + +#scope_file + +// Emitted even when the game declares no entity types at all: the generators in +// entities.jai are stalled waiting for this name and would never come unstuck. +emit_entity_types :: (w: *Workspace) { + // Sorted by name so the generated table — and with it the order of the + // editor's spawn list — does not shuffle between builds just because + // typechecking happened to finish in a different order. + quick_sort(entity_type_names, compare_strings); + + builder : String_Builder; + append(*builder, "ENTITY_TYPES :: Type.["); + for entity_type_names { + if it_index > 0 then append(*builder, ", "); + append(*builder, it); + } + append(*builder, "];\n"); + add_build_string(builder_to_string(*builder), w.*); +} + +struct_embeds_entity :: (s: *Code_Struct) -> bool { + if !s.defined_type then return false; + for s.defined_type.members { + if !(it.flags & .USING) then continue; + if it.type.type != .STRUCT then continue; + if (cast(*Type_Info_Struct) it.type).name == "Entity" then return true; + } + return false; +} + +// Game and engine code only. Module code never declares entity types, and +// build strings have no enclosing load to ask about. +is_main_program :: (decl: *Code_Declaration) -> bool { + if !decl.enclosing_load then return false; + if !decl.enclosing_load.enclosing_import then return false; + return decl.enclosing_load.enclosing_import.module_type == .MAIN_PROGRAM; +} + +report :: (decl: *Code_Declaration, error: string) { + compiler_report(error, make_location(cast(*Code_Node) decl), .ERROR); +} diff --git a/src/meta/meta.jai b/src/meta/meta.jai index fc74a41..aeebb46 100644 --- a/src/meta/meta.jai +++ b/src/meta/meta.jai @@ -1,8 +1,10 @@ #load "ascii.jai"; +#load "../resource_mirror.jai"; #load "pack.jai"; #load "lint.jai"; #load "hacks.jai"; #load "console_commands.jai"; +#load "entity_types.jai"; #load "shaderload.jai"; endframe_modified := false; @@ -14,4 +16,6 @@ custom_message_handler :: (message: *Message, w: *Workspace) { remove_getrect_custom_cursor(message, w); add_console_commands(message, w); + + add_entity_types(message, w); } diff --git a/src/meta/pack.jai b/src/meta/pack.jai index 6930910..bf8fd53 100644 --- a/src/meta/pack.jai +++ b/src/meta/pack.jai @@ -3,11 +3,9 @@ should_ignore_file :: (name: string) -> bool { if name.count > 0 && name[0] == #char "." return true; - ok, left, right := split_from_right(name, #char "."); - if right == "aseprite" { - print("Ignoring % as aseprite file...\n", name); - return true; - } + // Aseprite sources are inputs (see resource_mirror.jai), not shippable + // assets; only the .sheet.png/.sheet.json they export go into the pack. + if is_aseprite_source(name) return true; return false; } @@ -53,11 +51,10 @@ create_pack :: (include_test_resources: bool = false, pack_dir: string = "./pack #import "Simple_Package"; #import "File"; - util.visit_files("./resources", true, true, file_visit_handler); - if include_test_resources { - util.visit_files("./test_game/resources", true, true, file_visit_handler); - } else { - util.visit_files("./game/resources", true, true, file_visit_handler); + // Packs come from the processed copy of the resource trees, never the trees + // themselves. See resource_mirror.jai. + for prepare_resource_mirror(include_test_resources) { + util.visit_files(it, true, true, file_visit_handler); } make_directory_if_it_does_not_exist(pack_dir); diff --git a/src/orientation.jai b/src/orientation.jai new file mode 100644 index 0000000..e71437e --- /dev/null +++ b/src/orientation.jai @@ -0,0 +1,291 @@ +// The 24 axis-aligned cube rotations, encoded as 'face * 4 + twist' exactly as +// the trile shaders decode them (see get_orientation_matrix in +// shader_trile_shadow.glsl). Everything here mirrors that decode, so a rotation +// applied to a position on the CPU lines up with the geometry the GPU draws. +// +// Note the shaders build their mat3s from columns, which makes each of their +// rot_* helpers the transpose of the usual form. The matrices below are the +// effective row-major operators (v' = M * v), so they match what the shader +// actually does rather than what its helper names suggest. + +ORIENTATION_COUNT :: 24; + +Orientation_Matrix :: struct { + m : [3][3]s8; +} + +orientation_matrix :: (ori: u8) -> Orientation_Matrix { + return ORIENTATION_MATRICES[ori % ORIENTATION_COUNT]; +} + +rotate_by_orientation :: (ori: u8, v: Vector3) -> Vector3 { + m := orientation_matrix(ori).m; + return .{ + cast(float)m[0][0] * v.x + cast(float)m[0][1] * v.y + cast(float)m[0][2] * v.z, + cast(float)m[1][0] * v.x + cast(float)m[1][1] * v.y + cast(float)m[1][2] * v.z, + cast(float)m[2][0] * v.x + cast(float)m[2][1] * v.y + cast(float)m[2][2] * v.z, + }; +} + +// The orientation equal to applying 'inner' first and then 'outer'. Used to fold +// an entity's rotation into the orientation its parts were authored with. +compose_orientations :: (outer: u8, inner: u8) -> u8 { + return ORIENTATION_COMPOSE[outer % ORIENTATION_COUNT][inner % ORIENTATION_COUNT]; +} + +// The quarter turn that carries 'u' onto 'v' and leaves 'n' alone, where u, v, n +// are a set of perpendicular unit axes. Taking the basis from the caller keeps +// the result consistent with whatever handedness the caller draws with, instead +// of relying on a sign convention agreed at a distance. +orientation_quarter_turn :: (u: Vector3, v: Vector3, n: Vector3) -> u8 { + // M = v*uT - u*vT + n*nT sends u -> v, v -> -u, n -> n. + m : Orientation_Matrix; + ui := to_axis_ints(u); + vi := to_axis_ints(v); + ni := to_axis_ints(n); + for i: 0..2 { + for j: 0..2 { + m.m[i][j] = vi[i]*ui[j] - ui[i]*vi[j] + ni[i]*ni[j]; + } + } + ori, ok := orientation_from_matrix(m); + if !ok { + log_error("Rotate gizmo basis is not axis-aligned; ignoring the turn."); + return 0; + } + return ori; +} + +orientation_from_matrix :: (m: Orientation_Matrix) -> (ori: u8, ok: bool) { + for i: 0..ORIENTATION_COUNT-1 { + if mat3_equal(m, ORIENTATION_MATRICES[i]) return cast(u8) i, true; + } + return 0, false; +} + +#scope_file + +ORIENTATION_MATRICES :: #run generate_orientation_matrices(); +ORIENTATION_COMPOSE :: #run generate_orientation_compose_table(); + +// Axis vectors only ever hold 0 or +-1 here, but they arrive as floats. +to_axis_ints :: (v: Vector3) -> [3]s8 { + round_axis :: (f: float) -> s8 { + if f > 0.5 then return 1; + if f < -0.5 then return -1; + return 0; + } + return .[round_axis(v.x), round_axis(v.y), round_axis(v.z)]; +} + +mat3_of :: (a: s8, b: s8, c: s8, d: s8, e: s8, f: s8, g: s8, h: s8, i: s8) -> Orientation_Matrix { + r : Orientation_Matrix; + r.m[0][0] = a; r.m[0][1] = b; r.m[0][2] = c; + r.m[1][0] = d; r.m[1][1] = e; r.m[1][2] = f; + r.m[2][0] = g; r.m[2][1] = h; r.m[2][2] = i; + return r; +} + +// (c, s) are cos and sin of the angle, always in {-1, 0, 1} here. +rot_x_effective :: (s: s8, c: s8) -> Orientation_Matrix { return mat3_of(1, 0, 0, 0, c, s, 0, -s, c); } +rot_y_effective :: (s: s8, c: s8) -> Orientation_Matrix { return mat3_of(c, 0, -s, 0, 1, 0, s, 0, c); } +rot_z_effective :: (s: s8, c: s8) -> Orientation_Matrix { return mat3_of(c, s, 0, -s, c, 0, 0, 0, 1); } + +mat3_mul :: (a: Orientation_Matrix, b: Orientation_Matrix) -> Orientation_Matrix { + r : Orientation_Matrix; + for i: 0..2 { + for j: 0..2 { + sum : s8 = 0; + for k: 0..2 sum += a.m[i][k] * b.m[k][j]; + r.m[i][j] = sum; + } + } + return r; +} + +mat3_equal :: (a: Orientation_Matrix, b: Orientation_Matrix) -> bool { + for i: 0..2 for j: 0..2 if a.m[i][j] != b.m[i][j] return false; + return true; +} + +// Mirrors get_orientation_matrix: a face rotation followed by a twist about Y. +generate_orientation_matrices :: () -> [ORIENTATION_COUNT]Orientation_Matrix { + identity :: #run mat3_of(1, 0, 0, 0, 1, 0, 0, 0, 1); + // sin/cos of face and twist angles, written out to keep this exact. + faces : [6]Orientation_Matrix; + faces[0] = identity; + faces[1] = rot_x_effective( 0, -1); // PI + faces[2] = rot_z_effective(-1, 0); // -PI/2 + faces[3] = rot_z_effective( 1, 0); // PI/2 + faces[4] = rot_x_effective( 1, 0); // PI/2 + faces[5] = rot_x_effective(-1, 0); // -PI/2 + + twists : [4]Orientation_Matrix; + twists[0] = identity; + twists[1] = rot_y_effective( 1, 0); + twists[2] = rot_y_effective( 0, -1); + twists[3] = rot_y_effective(-1, 0); + + result : [ORIENTATION_COUNT]Orientation_Matrix; + for face: 0..5 { + for twist: 0..3 { + result[face * 4 + twist] = mat3_mul(faces[face], twists[twist]); + } + } + return result; +} + +generate_orientation_compose_table :: () -> [ORIENTATION_COUNT][ORIENTATION_COUNT]u8 { + mats := generate_orientation_matrices(); + table : [ORIENTATION_COUNT][ORIENTATION_COUNT]u8; + for a: 0..ORIENTATION_COUNT-1 { + for b: 0..ORIENTATION_COUNT-1 { + product := mat3_mul(mats[a], mats[b]); + found := false; + for c: 0..ORIENTATION_COUNT-1 { + if mat3_equal(product, mats[c]) { + table[a][b] = cast(u8) c; + found = true; + break; + } + } + assert(found, "Cube rotation % * % is not one of the 24 orientations", a, b); + } + } + return table; +} + + +#if FLAG_TEST_ENGINE { + v3_eq :: (a: Vector3, b: Vector3) -> bool { + return abs(a.x - b.x) < 0.001 && abs(a.y - b.y) < 0.001 && abs(a.z - b.z) < 0.001; + } + + test_orientation_set :: () { + s := begin_suite("cube orientations"); + + distinct := true; + for i: 0..ORIENTATION_COUNT-1 { + for j: i+1..ORIENTATION_COUNT-1 { + if mat3_equal(ORIENTATION_MATRICES[i], ORIENTATION_MATRICES[j]) distinct = false; + } + } + check(*s, "all 24 orientations are distinct", distinct); + + // Rotations preserve handedness, so every determinant must be +1. A -1 + // would mean the set had picked up a reflection. + proper := true; + for i: 0..ORIENTATION_COUNT-1 { + m := ORIENTATION_MATRICES[i].m; + det := cast(int)m[0][0] * (cast(int)m[1][1]*m[2][2] - cast(int)m[1][2]*m[2][1]) + - cast(int)m[0][1] * (cast(int)m[1][0]*m[2][2] - cast(int)m[1][2]*m[2][0]) + + cast(int)m[0][2] * (cast(int)m[1][0]*m[2][1] - cast(int)m[1][1]*m[2][0]); + if det != 1 proper = false; + } + check(*s, "every orientation is a proper rotation", proper); + + check(*s, "orientation 0 is the identity", + v3_eq(rotate_by_orientation(0, .{1, 2, 3}), .{1, 2, 3})); + + // Orientations 1..3 are the twists: rotations about Y, so Y is fixed. + twists_about_y := true; + for ori: 1..3 { + if !v3_eq(rotate_by_orientation(cast(u8)ori, .{0, 1, 0}), .{0, 1, 0}) twists_about_y = false; + } + check(*s, "the twist orientations turn about Y", twists_about_y); + + // Rotations are rigid: lengths survive. + lengths_kept := true; + for ori: 0..ORIENTATION_COUNT-1 { + r := rotate_by_orientation(cast(u8)ori, .{1, 2, 3}); + if abs(length(r) - length(Vector3.{1, 2, 3})) > 0.001 lengths_kept = false; + } + check(*s, "rotating preserves length", lengths_kept); + + // Pinned against the shader's decode. If these drift, entity part offsets + // will rotate the opposite way from the trile geometry they belong to. + // Orientation 1 is face 0 twist 1, the shader's rot_y(PI/2). + check(*s, "orientation 1 (twist 90) sends X to Z", + v3_eq(rotate_by_orientation(1, .{1, 0, 0}), .{0, 0, 1})); + // Orientation 4 is face 1 twist 0, the shader's rot_x(PI). + check(*s, "orientation 4 (face 1) flips Y and Z", + v3_eq(rotate_by_orientation(4, .{0, 1, 1}), .{0, -1, -1})); + // Orientation 16 is face 4 twist 0, the shader's rot_x(PI/2). + check(*s, "orientation 16 (face 4) sends Y to -Z", + v3_eq(rotate_by_orientation(16, .{0, 1, 0}), .{0, 0, -1})); + end_suite(s); + } + + test_orientation_compose :: () { + s := begin_suite("orientation composition"); + + identity_ok := true; + for ori: 0..ORIENTATION_COUNT-1 { + if compose_orientations(0, cast(u8)ori) != cast(u8)ori identity_ok = false; + if compose_orientations(cast(u8)ori, 0) != cast(u8)ori identity_ok = false; + } + check(*s, "composing with orientation 0 changes nothing", identity_ok); + + // Composing a fixed orientation with each of the 24 must hit each exactly + // once, or the table is not a group and some rotations are unreachable. + permutation := true; + for a: 0..ORIENTATION_COUNT-1 { + seen : [ORIENTATION_COUNT]bool; + for b: 0..ORIENTATION_COUNT-1 { + r := compose_orientations(cast(u8)a, cast(u8)b); + if seen[r] permutation = false; + seen[r] = true; + } + } + check(*s, "every row of the compose table is a permutation", permutation); + + // compose(outer, inner) must mean "inner first, then outer". + order_ok := true; + v := Vector3.{1, 2, 3}; + for a: 0..ORIENTATION_COUNT-1 { + for b: 0..ORIENTATION_COUNT-1 { + combined := rotate_by_orientation(compose_orientations(cast(u8)a, cast(u8)b), v); + stepwise := rotate_by_orientation(cast(u8)a, rotate_by_orientation(cast(u8)b, v)); + if !v3_eq(combined, stepwise) order_ok = false; + } + } + check(*s, "composing matches applying inner then outer", order_ok); + end_suite(s); + } + + test_orientation_quarter_turn :: () { + s := begin_suite("orientation quarter turns"); + X :: Vector3.{1, 0, 0}; + Y :: Vector3.{0, 1, 0}; + Z :: Vector3.{0, 0, 1}; + + q := orientation_quarter_turn(X, Z, Y); // turn X toward Z about Y + check(*s, "a quarter turn carries u onto v", v3_eq(rotate_by_orientation(q, X), Z)); + check(*s, "a quarter turn carries v onto -u", v3_eq(rotate_by_orientation(q, Z), .{-1, 0, 0})); + check(*s, "a quarter turn leaves its axis alone", v3_eq(rotate_by_orientation(q, Y), Y)); + + four := compose_orientations(q, compose_orientations(q, compose_orientations(q, q))); + check(*s, "four quarter turns return to the start", four == 0); + + // Dragging the ring back the other way must undo the turn exactly. + back := orientation_quarter_turn(Z, X, Y); + check(*s, "the reverse turn cancels the forward one", compose_orientations(q, back) == 0); + + all_valid := true; + for pair: Vector3.[X, Y, Z] { + u := pair; + v := ifx v3_eq(u, X) then Y else ifx v3_eq(u, Y) then Z else X; + n := cross(u, v); + t := orientation_quarter_turn(u, v, n); + if !v3_eq(rotate_by_orientation(t, u), v) all_valid = false; + } + check(*s, "quarter turns work on every axis pair", all_valid); + end_suite(s); + } + + #run { + test_orientation_set(); + test_orientation_compose(); + test_orientation_quarter_turn(); + } +} diff --git a/src/pack_hotreload.jai b/src/pack_hotreload.jai index 2e725a1..0804a00 100644 --- a/src/pack_hotreload.jai +++ b/src/pack_hotreload.jai @@ -1,5 +1,7 @@ #if !FLAG_RELEASE_BUILD && OS != .WASM { +#load "resource_mirror.jai"; + #import "String"; Pack_Writer :: #import "Simple_Package"; File_Util :: #import "File_Utilities"; @@ -36,8 +38,11 @@ _hotreload_visitor :: (info: *File_Util.File_Visit_Info, packs: *Table(string, P recreate_packs_on_disk :: () { packs: Table(string, Pack_Writer.Create_Package); - File_Util.visit_files("./resources", true, *packs, _hotreload_visitor); - File_Util.visit_files(GAME_RESOURCES_DIR, true, *packs, _hotreload_visitor); + // Same mirror-and-process pipeline the build uses, so a hot reload picks up + // edited .aseprite files too, not just plain assets. + for prepare_resource_mirror(FLAG_USE_TEST_GAME) { + File_Util.visit_files(it, true, *packs, _hotreload_visitor); + } for pack, key: packs { Pack_Writer.write(*pack, tprint("%/%.pack", PACK_DIR, key)); log_info("Hot-reload: wrote pack '%'", key); diff --git a/src/rendering/backend.jai b/src/rendering/backend.jai index eb8c8eb..3688a9d 100644 --- a/src/rendering/backend.jai +++ b/src/rendering/backend.jai @@ -17,6 +17,7 @@ Render_Command_Type :: enum { SET_CAMERA; DRAW_GROUND; ADD_TRILE_POSITIONS; + ADD_TRILE_POSITIONS_F32; DRAW_TRILE_POSITIONS; ADD_TRILE_RDM_POSITION; DRAW_TRILE_RDM; @@ -51,6 +52,15 @@ Render_Command_Add_Trile_Positions :: struct { chunk : Chunk_Key; } +// Like Add_Trile_Positions, but takes float world-space positions directly +// (xyz = position, w = orientation 0..23). Appends into the same instance +// buffer / offset list, so draws go through Render_Command_Draw_Trile_Positions. +Render_Command_Add_Trile_Positions_F32 :: struct { + #as using c : Render_Command; + c.type = .ADD_TRILE_POSITIONS_F32; + positions : []Vector4; +} + Render_Command_Draw_Trile_Positions :: struct { #as using c : Render_Command; c.type = .DRAW_TRILE_POSITIONS; diff --git a/src/rendering/backend_sokol.jai b/src/rendering/backend_sokol.jai index 16cdd89..dfd9e95 100644 --- a/src/rendering/backend_sokol.jai +++ b/src/rendering/backend_sokol.jai @@ -22,6 +22,9 @@ backend_handle_command :: (cmd: *Render_Command) { case .ADD_TRILE_POSITIONS; add_command := cast(*Render_Command_Add_Trile_Positions)cmd; backend_add_trile_positions(add_command.positions, add_command.chunk); + case .ADD_TRILE_POSITIONS_F32; + add_f32_command := cast(*Render_Command_Add_Trile_Positions_F32)cmd; + backend_add_trile_positions_f32(add_f32_command.positions); case .DRAW_TRILE_POSITIONS; draw_command := cast(*Render_Command_Draw_Trile_Positions)cmd; backend_draw_trile_positions(draw_command.trile, draw_command.amount, draw_command.conf, draw_command.chunk_key, draw_command.preview_mode, draw_command.offset_index, draw_command.lod_index); @@ -157,6 +160,14 @@ backend_add_trile_positions :: (positions : []Trile_Instance, chunk: Chunk_Key) array_add(*trile_offsets, offset); } +backend_add_trile_positions_f32 :: (positions : []Vector4) { + offset := sg_append_buffer(gPipelines.trile.bind.vertex_buffers[3], *(sg_range.{ + ptr = positions.data, + size = size_of(Vector4) * cast(u64)positions.count, + })); + array_add(*trile_offsets, offset); +} + backend_draw_trile_positions :: (trile : string, amount : s32, worldConf: *World_Config, chunk_key: Chunk_Key, preview_mode: s32 = 0, offset_index: s32 = 0, lod_index: s32 = 0) { if in_gbuffer_pass { backend_draw_trile_positions_gbuffer(trile, amount, worldConf, offset_index, lod_index); diff --git a/src/rendering/debug_draw.jai b/src/rendering/debug_draw.jai index c3cb1e3..375a5b5 100644 --- a/src/rendering/debug_draw.jai +++ b/src/rendering/debug_draw.jai @@ -1,8 +1,26 @@ -DEBUG_LINE_MAX :: 65536; +DEBUG_LINE_MAX :: 65536; +DEBUG_OVERLAY_LINE_MAX :: 8192; -g_debug_line_verts : [DEBUG_LINE_MAX * 2 * 7]float; +// One record per line, uploaded as instance data: the line pipeline expands each +// into a screen-space quad so lines can have a real pixel width. +Debug_Line :: struct { + a : Vector3; + b : Vector3; + color : Vector4; + width : float; // in pixels +} + +DEBUG_LINE_DEFAULT_WIDTH :: 1.0; + +g_debug_lines : [DEBUG_LINE_MAX]Debug_Line; g_debug_line_count : int; +// Overlay lines ignore the depth buffer, so editor gizmos stay visible through +// terrain. They are UI rather than debug visualization, so they are not gated +// on debug_draw_enabled. +g_overlay_lines : [DEBUG_OVERLAY_LINE_MAX]Debug_Line; +g_overlay_line_count : int; + debug_draw_enabled : bool = !FLAG_RELEASE_BUILD; debug_draw_grid : bool = true; debug_draw_vectors : bool = true; @@ -24,27 +42,35 @@ toggle_debug_colliders :: () { debug_draw_colliders = !debug_draw_colliders; } @Command -debug_line :: (a: Vector3, b: Vector3, col: Vector4) { +debug_line :: (a: Vector3, b: Vector3, col: Vector4, width: float = DEBUG_LINE_DEFAULT_WIDTH) { if !debug_draw_enabled then return; if g_debug_line_count >= DEBUG_LINE_MAX then return; - base := g_debug_line_count * 14; - g_debug_line_verts[base + 0] = a.x; - g_debug_line_verts[base + 1] = a.y; - g_debug_line_verts[base + 2] = a.z; - g_debug_line_verts[base + 3] = col.x; - g_debug_line_verts[base + 4] = col.y; - g_debug_line_verts[base + 5] = col.z; - g_debug_line_verts[base + 6] = col.w; - g_debug_line_verts[base + 7] = b.x; - g_debug_line_verts[base + 8] = b.y; - g_debug_line_verts[base + 9] = b.z; - g_debug_line_verts[base + 10] = col.x; - g_debug_line_verts[base + 11] = col.y; - g_debug_line_verts[base + 12] = col.z; - g_debug_line_verts[base + 13] = col.w; + g_debug_lines[g_debug_line_count] = .{a, b, col, width}; g_debug_line_count += 1; } +debug_line_overlay :: (a: Vector3, b: Vector3, col: Vector4, width: float = DEBUG_LINE_DEFAULT_WIDTH) { + if g_overlay_line_count >= DEBUG_OVERLAY_LINE_MAX then return; + g_overlay_lines[g_overlay_line_count] = .{a, b, col, width}; + g_overlay_line_count += 1; +} + +debug_aabb_3d_overlay :: (mn: Vector3, mx: Vector3, col: Vector4, width: float = DEBUG_LINE_DEFAULT_WIDTH) { + corner :: (mn: Vector3, mx: Vector3, i: int) -> Vector3 { + return .{ ifx i & 1 then mx.x else mn.x, + ifx i & 2 then mx.y else mn.y, + ifx i & 4 then mx.z else mn.z }; + } + // Every pair of corners differing in exactly one bit is an edge of the box. + for i: 0..7 { + for bit: int.[1, 2, 4] { + j := i | bit; + if j == i then continue; + debug_line_overlay(corner(mn, mx, i), corner(mn, mx, j), col, width); + } + } +} + debug_vector :: (origin: Vector3, vec: Vector3, col: Vector4) { if !debug_draw_enabled || !debug_draw_vectors then return; tip := origin + vec; @@ -126,21 +152,34 @@ debug_aabb_3d :: (mn: Vector3, mx: Vector3, col: Vector4) { debug_line(.{mx.x, mn.y, mx.z}, .{mx.x, mx.y, mx.z}, col); } -debug_draw_flush_gpu :: () { - if g_debug_line_count == 0 then return; +flush_debug_lines :: (pipe: *Pipeline_Binding, lines: []Debug_Line, count: *int, params: *Debugline_Vs_Params) { + if count.* == 0 then return; + // Buffer 0 is the shared static quad; buffer 1 is this pipeline's instances. sg_update_buffer( - gPipelines.debugline.bind.vertex_buffers[0], + pipe.bind.vertex_buffers[1], *(sg_range.{ - ptr = g_debug_line_verts.data, - size = cast(u64)(g_debug_line_count * 14 * size_of(float)), + ptr = lines.data, + size = cast(u64)(count.* * size_of(Debug_Line)), }) ); - mvp := create_viewproj(*camera); + sg_apply_pipeline(pipe.pipeline); + sg_apply_bindings(*pipe.bind); + sg_apply_uniforms(UB_debugline_vs_params, *(sg_range.{ ptr = params, size = size_of(Debugline_Vs_Params) })); + sg_draw(0, 6, xx count.*); + count.* = 0; +} + +debug_draw_flush_gpu :: () { + if g_debug_line_count == 0 && g_overlay_line_count == 0 then return; + + w, h := get_window_size(); + mvp := create_viewproj(*camera); params : Debugline_Vs_Params; params.mvp = mvp.floats; - sg_apply_pipeline(gPipelines.debugline.pipeline); - sg_apply_bindings(*gPipelines.debugline.bind); - sg_apply_uniforms(UB_debugline_vs_params, *(sg_range.{ ptr = *params, size = size_of(Debugline_Vs_Params) })); - sg_draw(0, xx (g_debug_line_count * 2), 1); - g_debug_line_count = 0; + params.viewport = .[cast(float)w, cast(float)h, 0, 0]; + + flush_debug_lines(*gPipelines.debugline, g_debug_lines, *g_debug_line_count, *params); + flush_debug_lines(*gPipelines.debugline_overlay, g_overlay_lines, *g_overlay_line_count, *params); } + +#assert size_of(Debug_Line) == 44 "Debug_Line must stay tightly packed: the pipeline layout hardcodes its field offsets."; diff --git a/src/rendering/pipelines.jai b/src/rendering/pipelines.jai index 3b803ce..2c50c87 100644 --- a/src/rendering/pipelines.jai +++ b/src/rendering/pipelines.jai @@ -113,6 +113,8 @@ gPipelines : struct { sh_irradiance: Pipeline_Binding; debugline : Pipeline_Binding; + // Same shader, but depth testing disabled so editor gizmos draw on top. + debugline_overlay : Pipeline_Binding; } create_final_image :: () { @@ -1694,20 +1696,45 @@ create_particle_pipeline :: () { } create_debugline_pipeline :: () { - buf_desc := sg_buffer_desc.{ - size = DEBUG_LINE_MAX * 2 * 7 * size_of(float), - usage = .DYNAMIC, - label = "debug_line_verts", + // The two corner coordinates of a quad: x picks the side of the line, y picks + // the end. The vertex shader turns those into a screen-space widened line. + corners := float.[ + -1, 0, 1, 0, 1, 1, + -1, 0, 1, 1, -1, 1, + ]; + quad_desc := sg_buffer_desc.{ + data = .{ ptr = corners.data, size = size_of(type_of(corners)) }, + label = "debug_line_quad", }; - gPipelines.debugline.bind.vertex_buffers[0] = sg_make_buffer(*buf_desc); + quad_buffer := sg_make_buffer(*quad_desc); + + buf_desc := sg_buffer_desc.{ + size = DEBUG_LINE_MAX * size_of(Debug_Line), + usage = .DYNAMIC, + label = "debug_line_instances", + }; + gPipelines.debugline.bind.vertex_buffers[0] = quad_buffer; + gPipelines.debugline.bind.vertex_buffers[1] = sg_make_buffer(*buf_desc); + + overlay_buf_desc := sg_buffer_desc.{ + size = DEBUG_OVERLAY_LINE_MAX * size_of(Debug_Line), + usage = .DYNAMIC, + label = "debug_line_overlay_instances", + }; + gPipelines.debugline_overlay.bind.vertex_buffers[0] = quad_buffer; + gPipelines.debugline_overlay.bind.vertex_buffers[1] = sg_make_buffer(*overlay_buf_desc); pipeline : sg_pipeline_desc; shader_desc := debugline_shader_desc(sg_query_backend()); pipeline.shader = sg_make_shader(*shader_desc); - pipeline.primitive_type = .LINES; - pipeline.layout.buffers[0].stride = 28; - pipeline.layout.attrs[ATTR_debugline_a_pos] = .{ format = .FLOAT3, buffer_index = 0, offset = 0 }; - pipeline.layout.attrs[ATTR_debugline_a_col] = .{ format = .FLOAT4, buffer_index = 0, offset = 12 }; + pipeline.layout.buffers[0].stride = 2 * size_of(float); + pipeline.layout.buffers[1].stride = size_of(Debug_Line); + pipeline.layout.buffers[1].step_func = .PER_INSTANCE; + pipeline.layout.attrs[ATTR_debugline_a_corner] = .{ format = .FLOAT2, buffer_index = 0, offset = 0 }; + pipeline.layout.attrs[ATTR_debugline_a_pos_a] = .{ format = .FLOAT3, buffer_index = 1, offset = 0 }; + pipeline.layout.attrs[ATTR_debugline_a_pos_b] = .{ format = .FLOAT3, buffer_index = 1, offset = 12 }; + pipeline.layout.attrs[ATTR_debugline_a_col] = .{ format = .FLOAT4, buffer_index = 1, offset = 24 }; + pipeline.layout.attrs[ATTR_debugline_a_width] = .{ format = .FLOAT, buffer_index = 1, offset = 40 }; pipeline.depth = .{ write_enabled = false, compare = .LESS_EQUAL, @@ -1720,4 +1747,13 @@ create_debugline_pipeline :: () { pipeline.colors[0] = color_state; pipeline.label = "debugline_pipeline"; gPipelines.debugline.pipeline = sg_make_pipeline(*pipeline); + + pipeline.depth.compare = .ALWAYS; + pipeline.colors[0].blend = .{ + enabled = true, + src_factor_rgb = .SRC_ALPHA, + dst_factor_rgb = .ONE_MINUS_SRC_ALPHA, + }; + pipeline.label = "debugline_overlay_pipeline"; + gPipelines.debugline_overlay.pipeline = sg_make_pipeline(*pipeline); } diff --git a/src/rendering/tasks.jai b/src/rendering/tasks.jai index c203ee5..74b5453 100644 --- a/src/rendering/tasks.jai +++ b/src/rendering/tasks.jai @@ -7,6 +7,7 @@ Rendering_Task_Type :: enum { SET_CAMERA; SET_LIGHT; TRILE; // We need to add an ability to invalidate buffer instead of updating it constantly. Also probably have a buffer for static world triles and one for moving ones. + TRILE_DYNAMIC; TRILE_RDM; TRIXELS; BILLBOARD; @@ -65,6 +66,19 @@ Rendering_Task_Trile :: struct { lod_index : s32 = 0; // 0 = full detail, 1 = 4^3 LOD, 2 = 2^3 LOD } +// Triles at arbitrary float world positions (entities, moving things). +// Shares the instance buffer and pipelines with the static TRILE path, so it +// participates in main/gbuffer/shadow/reflection just like world triles. +// chunk_key is the enclosing chunk, used only for SH probe lookup. +Rendering_Task_Trile_Dynamic :: struct { + #as using t : Rendering_Task; + t.type = .TRILE_DYNAMIC; + trile : string; + positions : []Vector4; // xyz = world position, w = orientation 0..23 + chunk_key : Chunk_Key; + worldConf : *World_Config; +} + Rendering_Task_Trile_RDM :: struct { #as using t : Rendering_Task; t.type = .TRILE_RDM; @@ -177,6 +191,22 @@ tasks_to_commands :: () { array_add(*render_command_buckets.gbuffer, drawPositionsCmd); array_add(*render_command_buckets.shadow, drawPositionsCmd); } + case .TRILE_DYNAMIC; + dynTask := (cast(*Rendering_Task_Trile_Dynamic)it); + addF32Cmd := New(Render_Command_Add_Trile_Positions_F32,, temp); + addF32Cmd.positions = dynTask.positions; + array_add(*render_command_buckets.setup, addF32Cmd); + drawDynCmd := New(Render_Command_Draw_Trile_Positions,, temp); + drawDynCmd.trile = dynTask.trile; + drawDynCmd.chunk_key = dynTask.chunk_key; + drawDynCmd.amount = cast(s32)dynTask.positions.count; + drawDynCmd.conf = dynTask.worldConf; + drawDynCmd.offset_index = trile_add_counter; + trile_add_counter += 1; + array_add(*render_command_buckets.reflection, drawDynCmd); + array_add(*render_command_buckets.main, drawDynCmd); + array_add(*render_command_buckets.gbuffer, drawDynCmd); + array_add(*render_command_buckets.shadow, drawDynCmd); case .TRILE_RDM; rdmTask := (cast(*Rendering_Task_Trile_RDM)it); addCmd := New(Render_Command_Add_Trile_RDM_Position,, temp); diff --git a/src/resource_mirror.jai b/src/resource_mirror.jai new file mode 100644 index 0000000..ad626ab --- /dev/null +++ b/src/resource_mirror.jai @@ -0,0 +1,162 @@ +// Packs are built from a throwaway copy of the resource trees, not from the +// trees themselves: +// +// wipe .build/generated -> copy the trees in -> process the copy -> pack it +// +// Processing (currently just the Aseprite export) writes its output into the +// copy, so the real resource trees only ever hold sources. Rebuilding the copy +// from scratch every time is what keeps this simple: there is no stale output to +// detect and nothing to prune, because a deleted source is simply not there. +// +// Used by both the build (meta/pack.jai) and the runtime hot-reloader +// (pack_hotreload.jai), so they process resources identically. + +#import "Basic"; +#import "String"; +#import "Process"; +#import "File"; +#import "File_Utilities"; + +// Not ".build/generated/resources": pack.jai derives the pack name by splitting +// on the *first* "/resources/", so a "resources" segment here would shadow the +// real one and yield pack "game" instead of "game_core". +MIRROR_ROOT :: ".build/generated"; + +ASEPRITE_SOURCE_EXTENSIONS :: string.["aseprite", "ase"]; + +is_aseprite_source :: (short_name: string) -> bool { + ok, base, extension := split_from_right(short_name, #char "."); + if !ok return false; + for ASEPRITE_SOURCE_EXTENSIONS if extension == it return true; + return false; +} + +// Wipes and rebuilds the mirror, runs the processing steps over it, and returns +// the mirrored resource roots to pack in place of the real ones. +prepare_resource_mirror :: (use_test_game: bool) -> [] string { + run_command("rm", "-rf", MIRROR_ROOT); + + game_tree := ifx use_test_game then "test_game" else "game"; + + roots: [..] string; + roots.allocator = temp; + array_add(*roots, copy_tree_into("./resources", MIRROR_ROOT)); + array_add(*roots, copy_tree_into(tprint("./%/resources", game_tree), tprint("%/%", MIRROR_ROOT, game_tree))); + + export_aseprite_sheets(roots); + + return roots; +} + +// "cp -r ./game/resources .build/generated/game" -> ".build/generated/game/resources" +copy_tree_into :: (source: string, destination_parent: string) -> string { + make_directory_if_it_does_not_exist(destination_parent, recursive = true); + run_command("cp", "-r", source, destination_parent); + + ok, parent, name := split_from_right(source, #char "/"); + return tprint("%/%", destination_parent, name); +} + +// Aseprite's CLI, reproducing byte for byte what its "Export Sprite Sheet" +// dialog was producing for the sheets that used to be checked in. +// --format json-array and --list-tags are load-bearing: asset_manager.jai parses +// `frames` as an array and drives Animation off `meta.frameTags`. +ASEPRITE_EXPORT_FLAGS :: string.[ + "--format", "json-array", + "--list-tags", + "--list-layers", + "--list-slices", +]; + +// "player.aseprite" -> player.sheet.png / player.sheet.json +// "anim.sheet.aseprite" -> anim.sheet.png / anim.sheet.json +// +// The doubled-up ".sheet" is dropped because asset_manager.jai keys sheets on +// everything before the *first* dot, so "anim.sheet.sheet.png" would load as a +// sheet named "anim" with extension "sheet.sheet.png" and be rejected. +sheet_output_paths :: (source: string) -> (png: string, json: string) { + ok, stem := split_from_right(source, #char "."); + if ends_with(stem, ".sheet") { + stem.count -= ".sheet".count; + } + return tprint("%.sheet.png", stem), tprint("%.sheet.json", stem); +} + +find_aseprite_binary :: () -> (path: string, found: bool) { + candidates: [..] string; + candidates.allocator = temp; + + #if OS == .LINUX || OS == .MACOS { + POSIX :: #import "POSIX"; + read_env :: (name: string) -> string { + value := POSIX.getenv(name.data); + if !value return ""; + return to_string(value); + } + + // ASEPRITE points the build at a different install without editing this. + override := read_env("ASEPRITE"); + if override array_add(*candidates, override); + + home := read_env("HOME"); + if home array_add(*candidates, tprint("%/bin/aseprite", home)); + } + + #if OS == .MACOS { + array_add(*candidates, "/Applications/Aseprite.app/Contents/MacOS/aseprite"); + } + + array_add(*candidates, "aseprite"); // whatever is on PATH + + for candidates { + // Probing by running it also catches a path that exists but isn't executable. + result := run_command(it, "--version", capture_and_return_output = true); + if result.type == .EXITED && result.exit_code == 0 return it, true; + } + return "", false; +} + +collect_aseprite_sources :: (root: string, sources: *[..] string) { + if !is_directory(root) return; + + visit_files(root, true, sources, (info: *File_Visit_Info, sources: *[..] string) { + if info.is_directory return; + if !is_aseprite_source(info.short_name) return; + array_add(sources, copy_string(info.full_name)); + }); +} + +// Exports every Aseprite file in the mirror to a sheet pair beside it. pack.jai +// skips the sources themselves, so only the exports end up in the pack. +export_aseprite_sheets :: (roots: [] string) { + sources: [..] string; + sources.allocator = temp; + for roots collect_aseprite_sources(it, *sources); + if !sources return; + + aseprite, found := find_aseprite_binary(); + if !found { + log_error("Aseprite export: % source file(s) to export but no aseprite binary was found.", sources.count); + log_error("Looked for $ASEPRITE, $HOME/bin/aseprite, and 'aseprite' on PATH."); + exit(1); + } + + for source: sources { + png, json := sheet_output_paths(source); + + args: [..] string; + args.allocator = temp; + array_add(*args, aseprite, "-b", source, "--sheet", png, "--data", json); + for flag: ASEPRITE_EXPORT_FLAGS array_add(*args, flag); + + result, output, error := run_command(..args, capture_and_return_output = true); + if result.type != .EXITED || result.exit_code != 0 { + log_error("Aseprite export failed for %", source); + if output log_error(output,, logger = runtime_support_default_logger); + if error log_error(error,, logger = runtime_support_default_logger); + exit(1); + } + } + + print("Aseprite: exported % sheet(s)\n", sources.count); +} diff --git a/src/shaders/jai/shader_debugline.jai b/src/shaders/jai/shader_debugline.jai index 20126a6..8eb1711 100644 --- a/src/shaders/jai/shader_debugline.jai +++ b/src/shaders/jai/shader_debugline.jai @@ -13,30 +13,55 @@ Vertex Shader: vs_debugline Fragment Shader: fs_debugline Attributes: - ATTR_debugline_a_pos => 0 - ATTR_debugline_a_col => 1 + ATTR_debugline_a_corner => 0 + ATTR_debugline_a_pos_a => 1 + ATTR_debugline_a_pos_b => 2 + ATTR_debugline_a_col => 3 + ATTR_debugline_a_width => 4 Bindings: Uniform block 'debugline_vs_params': Jai struct: Debugline_Vs_Params Bind slot: UB_debugline_vs_params => 0 */ -ATTR_debugline_a_pos :: 0; -ATTR_debugline_a_col :: 1; +ATTR_debugline_a_corner :: 0; +ATTR_debugline_a_pos_a :: 1; +ATTR_debugline_a_pos_b :: 2; +ATTR_debugline_a_col :: 3; +ATTR_debugline_a_width :: 4; UB_debugline_vs_params :: 0; Debugline_Vs_Params :: struct { mvp: [16]float; + viewport: [4]float; }; /* #version 430 - uniform vec4 debugline_vs_params[4]; - layout(location = 0) in vec3 a_pos; + uniform vec4 debugline_vs_params[5]; + layout(location = 1) in vec3 a_pos_a; + layout(location = 2) in vec3 a_pos_b; + layout(location = 0) in vec2 a_corner; + layout(location = 4) in float a_width; layout(location = 0) out vec4 v_col; - layout(location = 1) in vec4 a_col; + layout(location = 3) in vec4 a_col; void main() { - gl_Position = mat4(debugline_vs_params[0], debugline_vs_params[1], debugline_vs_params[2], debugline_vs_params[3]) * vec4(a_pos, 1.0); + mat4 _18 = mat4(debugline_vs_params[0], debugline_vs_params[1], debugline_vs_params[2], debugline_vs_params[3]); + vec4 _28 = _18 * vec4(a_pos_a, 1.0); + vec4 _38 = _18 * vec4(a_pos_b, 1.0); + vec2 _78 = ((_38.xy / vec2(max(_38.w, 9.9999997473787516355514526367188e-05))) * debugline_vs_params[4].xy) - ((_28.xy / vec2(max(_28.w, 9.9999997473787516355514526367188e-05))) * debugline_vs_params[4].xy); + float _81 = length(_78); + vec2 _86; + if (_81 > 9.9999997473787516355514526367188e-05) + { + _86 = _78 / vec2(_81); + } + else + { + _86 = vec2(1.0, 0.0); + } + vec4 _115 = mix(_28, _38, vec4(a_corner.y)); + gl_Position = _115 + vec4((((vec2(-_86.y, _86.x) * a_corner.x) * ((a_width * 0.5) + 0.5)) / debugline_vs_params[4].xy) * _115.w, 0.0, 0.0); v_col = a_col; } @@ -44,26 +69,74 @@ Debugline_Vs_Params :: struct { vs_debugline_source_glsl430 := u8.[ 0x23,0x76,0x65,0x72,0x73,0x69,0x6f,0x6e,0x20,0x34,0x33,0x30,0x0a,0x0a,0x75,0x6e, 0x69,0x66,0x6f,0x72,0x6d,0x20,0x76,0x65,0x63,0x34,0x20,0x64,0x65,0x62,0x75,0x67, - 0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x5b,0x34, + 0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x5b,0x35, 0x5d,0x3b,0x0a,0x6c,0x61,0x79,0x6f,0x75,0x74,0x28,0x6c,0x6f,0x63,0x61,0x74,0x69, - 0x6f,0x6e,0x20,0x3d,0x20,0x30,0x29,0x20,0x69,0x6e,0x20,0x76,0x65,0x63,0x33,0x20, - 0x61,0x5f,0x70,0x6f,0x73,0x3b,0x0a,0x6c,0x61,0x79,0x6f,0x75,0x74,0x28,0x6c,0x6f, - 0x63,0x61,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x30,0x29,0x20,0x6f,0x75,0x74,0x20, - 0x76,0x65,0x63,0x34,0x20,0x76,0x5f,0x63,0x6f,0x6c,0x3b,0x0a,0x6c,0x61,0x79,0x6f, - 0x75,0x74,0x28,0x6c,0x6f,0x63,0x61,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x31,0x29, - 0x20,0x69,0x6e,0x20,0x76,0x65,0x63,0x34,0x20,0x61,0x5f,0x63,0x6f,0x6c,0x3b,0x0a, - 0x0a,0x76,0x6f,0x69,0x64,0x20,0x6d,0x61,0x69,0x6e,0x28,0x29,0x0a,0x7b,0x0a,0x20, + 0x6f,0x6e,0x20,0x3d,0x20,0x31,0x29,0x20,0x69,0x6e,0x20,0x76,0x65,0x63,0x33,0x20, + 0x61,0x5f,0x70,0x6f,0x73,0x5f,0x61,0x3b,0x0a,0x6c,0x61,0x79,0x6f,0x75,0x74,0x28, + 0x6c,0x6f,0x63,0x61,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x32,0x29,0x20,0x69,0x6e, + 0x20,0x76,0x65,0x63,0x33,0x20,0x61,0x5f,0x70,0x6f,0x73,0x5f,0x62,0x3b,0x0a,0x6c, + 0x61,0x79,0x6f,0x75,0x74,0x28,0x6c,0x6f,0x63,0x61,0x74,0x69,0x6f,0x6e,0x20,0x3d, + 0x20,0x30,0x29,0x20,0x69,0x6e,0x20,0x76,0x65,0x63,0x32,0x20,0x61,0x5f,0x63,0x6f, + 0x72,0x6e,0x65,0x72,0x3b,0x0a,0x6c,0x61,0x79,0x6f,0x75,0x74,0x28,0x6c,0x6f,0x63, + 0x61,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x34,0x29,0x20,0x69,0x6e,0x20,0x66,0x6c, + 0x6f,0x61,0x74,0x20,0x61,0x5f,0x77,0x69,0x64,0x74,0x68,0x3b,0x0a,0x6c,0x61,0x79, + 0x6f,0x75,0x74,0x28,0x6c,0x6f,0x63,0x61,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x30, + 0x29,0x20,0x6f,0x75,0x74,0x20,0x76,0x65,0x63,0x34,0x20,0x76,0x5f,0x63,0x6f,0x6c, + 0x3b,0x0a,0x6c,0x61,0x79,0x6f,0x75,0x74,0x28,0x6c,0x6f,0x63,0x61,0x74,0x69,0x6f, + 0x6e,0x20,0x3d,0x20,0x33,0x29,0x20,0x69,0x6e,0x20,0x76,0x65,0x63,0x34,0x20,0x61, + 0x5f,0x63,0x6f,0x6c,0x3b,0x0a,0x0a,0x76,0x6f,0x69,0x64,0x20,0x6d,0x61,0x69,0x6e, + 0x28,0x29,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x6d,0x61,0x74,0x34,0x20,0x5f,0x31, + 0x38,0x20,0x3d,0x20,0x6d,0x61,0x74,0x34,0x28,0x64,0x65,0x62,0x75,0x67,0x6c,0x69, + 0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x5b,0x30,0x5d,0x2c, + 0x20,0x64,0x65,0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70,0x61, + 0x72,0x61,0x6d,0x73,0x5b,0x31,0x5d,0x2c,0x20,0x64,0x65,0x62,0x75,0x67,0x6c,0x69, + 0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x5b,0x32,0x5d,0x2c, + 0x20,0x64,0x65,0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70,0x61, + 0x72,0x61,0x6d,0x73,0x5b,0x33,0x5d,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x76,0x65, + 0x63,0x34,0x20,0x5f,0x32,0x38,0x20,0x3d,0x20,0x5f,0x31,0x38,0x20,0x2a,0x20,0x76, + 0x65,0x63,0x34,0x28,0x61,0x5f,0x70,0x6f,0x73,0x5f,0x61,0x2c,0x20,0x31,0x2e,0x30, + 0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x76,0x65,0x63,0x34,0x20,0x5f,0x33,0x38,0x20, + 0x3d,0x20,0x5f,0x31,0x38,0x20,0x2a,0x20,0x76,0x65,0x63,0x34,0x28,0x61,0x5f,0x70, + 0x6f,0x73,0x5f,0x62,0x2c,0x20,0x31,0x2e,0x30,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20, + 0x76,0x65,0x63,0x32,0x20,0x5f,0x37,0x38,0x20,0x3d,0x20,0x28,0x28,0x5f,0x33,0x38, + 0x2e,0x78,0x79,0x20,0x2f,0x20,0x76,0x65,0x63,0x32,0x28,0x6d,0x61,0x78,0x28,0x5f, + 0x33,0x38,0x2e,0x77,0x2c,0x20,0x39,0x2e,0x39,0x39,0x39,0x39,0x39,0x39,0x37,0x34, + 0x37,0x33,0x37,0x38,0x37,0x35,0x31,0x36,0x33,0x35,0x35,0x35,0x31,0x34,0x35,0x32, + 0x36,0x33,0x36,0x37,0x31,0x38,0x38,0x65,0x2d,0x30,0x35,0x29,0x29,0x29,0x20,0x2a, + 0x20,0x64,0x65,0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70,0x61, + 0x72,0x61,0x6d,0x73,0x5b,0x34,0x5d,0x2e,0x78,0x79,0x29,0x20,0x2d,0x20,0x28,0x28, + 0x5f,0x32,0x38,0x2e,0x78,0x79,0x20,0x2f,0x20,0x76,0x65,0x63,0x32,0x28,0x6d,0x61, + 0x78,0x28,0x5f,0x32,0x38,0x2e,0x77,0x2c,0x20,0x39,0x2e,0x39,0x39,0x39,0x39,0x39, + 0x39,0x37,0x34,0x37,0x33,0x37,0x38,0x37,0x35,0x31,0x36,0x33,0x35,0x35,0x35,0x31, + 0x34,0x35,0x32,0x36,0x33,0x36,0x37,0x31,0x38,0x38,0x65,0x2d,0x30,0x35,0x29,0x29, + 0x29,0x20,0x2a,0x20,0x64,0x65,0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73, + 0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x5b,0x34,0x5d,0x2e,0x78,0x79,0x29,0x3b,0x0a, + 0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x20,0x5f,0x38,0x31,0x20,0x3d,0x20, + 0x6c,0x65,0x6e,0x67,0x74,0x68,0x28,0x5f,0x37,0x38,0x29,0x3b,0x0a,0x20,0x20,0x20, + 0x20,0x76,0x65,0x63,0x32,0x20,0x5f,0x38,0x36,0x3b,0x0a,0x20,0x20,0x20,0x20,0x69, + 0x66,0x20,0x28,0x5f,0x38,0x31,0x20,0x3e,0x20,0x39,0x2e,0x39,0x39,0x39,0x39,0x39, + 0x39,0x37,0x34,0x37,0x33,0x37,0x38,0x37,0x35,0x31,0x36,0x33,0x35,0x35,0x35,0x31, + 0x34,0x35,0x32,0x36,0x33,0x36,0x37,0x31,0x38,0x38,0x65,0x2d,0x30,0x35,0x29,0x0a, + 0x20,0x20,0x20,0x20,0x7b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x5f,0x38, + 0x36,0x20,0x3d,0x20,0x5f,0x37,0x38,0x20,0x2f,0x20,0x76,0x65,0x63,0x32,0x28,0x5f, + 0x38,0x31,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x7d,0x0a,0x20,0x20,0x20,0x20,0x65, + 0x6c,0x73,0x65,0x0a,0x20,0x20,0x20,0x20,0x7b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20, + 0x20,0x20,0x5f,0x38,0x36,0x20,0x3d,0x20,0x76,0x65,0x63,0x32,0x28,0x31,0x2e,0x30, + 0x2c,0x20,0x30,0x2e,0x30,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x7d,0x0a,0x20,0x20, + 0x20,0x20,0x76,0x65,0x63,0x34,0x20,0x5f,0x31,0x31,0x35,0x20,0x3d,0x20,0x6d,0x69, + 0x78,0x28,0x5f,0x32,0x38,0x2c,0x20,0x5f,0x33,0x38,0x2c,0x20,0x76,0x65,0x63,0x34, + 0x28,0x61,0x5f,0x63,0x6f,0x72,0x6e,0x65,0x72,0x2e,0x79,0x29,0x29,0x3b,0x0a,0x20, 0x20,0x20,0x20,0x67,0x6c,0x5f,0x50,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x20,0x3d, - 0x20,0x6d,0x61,0x74,0x34,0x28,0x64,0x65,0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f, - 0x76,0x73,0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x5b,0x30,0x5d,0x2c,0x20,0x64,0x65, - 0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70,0x61,0x72,0x61,0x6d, - 0x73,0x5b,0x31,0x5d,0x2c,0x20,0x64,0x65,0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f, - 0x76,0x73,0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x5b,0x32,0x5d,0x2c,0x20,0x64,0x65, - 0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70,0x61,0x72,0x61,0x6d, - 0x73,0x5b,0x33,0x5d,0x29,0x20,0x2a,0x20,0x76,0x65,0x63,0x34,0x28,0x61,0x5f,0x70, - 0x6f,0x73,0x2c,0x20,0x31,0x2e,0x30,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x76,0x5f, - 0x63,0x6f,0x6c,0x20,0x3d,0x20,0x61,0x5f,0x63,0x6f,0x6c,0x3b,0x0a,0x7d,0x0a,0x0a, - 0x00, + 0x20,0x5f,0x31,0x31,0x35,0x20,0x2b,0x20,0x76,0x65,0x63,0x34,0x28,0x28,0x28,0x28, + 0x76,0x65,0x63,0x32,0x28,0x2d,0x5f,0x38,0x36,0x2e,0x79,0x2c,0x20,0x5f,0x38,0x36, + 0x2e,0x78,0x29,0x20,0x2a,0x20,0x61,0x5f,0x63,0x6f,0x72,0x6e,0x65,0x72,0x2e,0x78, + 0x29,0x20,0x2a,0x20,0x28,0x28,0x61,0x5f,0x77,0x69,0x64,0x74,0x68,0x20,0x2a,0x20, + 0x30,0x2e,0x35,0x29,0x20,0x2b,0x20,0x30,0x2e,0x35,0x29,0x29,0x20,0x2f,0x20,0x64, + 0x65,0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70,0x61,0x72,0x61, + 0x6d,0x73,0x5b,0x34,0x5d,0x2e,0x78,0x79,0x29,0x20,0x2a,0x20,0x5f,0x31,0x31,0x35, + 0x2e,0x77,0x2c,0x20,0x30,0x2e,0x30,0x2c,0x20,0x30,0x2e,0x30,0x29,0x3b,0x0a,0x20, + 0x20,0x20,0x20,0x76,0x5f,0x63,0x6f,0x6c,0x20,0x3d,0x20,0x61,0x5f,0x63,0x6f,0x6c, + 0x3b,0x0a,0x7d,0x0a,0x0a,0x00, ]; /* #version 430 @@ -91,14 +164,32 @@ fs_debugline_source_glsl430 := u8.[ /* #version 300 es - uniform vec4 debugline_vs_params[4]; - layout(location = 0) in vec3 a_pos; + uniform vec4 debugline_vs_params[5]; + layout(location = 1) in vec3 a_pos_a; + layout(location = 2) in vec3 a_pos_b; + layout(location = 0) in vec2 a_corner; + layout(location = 4) in float a_width; out vec4 v_col; - layout(location = 1) in vec4 a_col; + layout(location = 3) in vec4 a_col; void main() { - gl_Position = mat4(debugline_vs_params[0], debugline_vs_params[1], debugline_vs_params[2], debugline_vs_params[3]) * vec4(a_pos, 1.0); + mat4 _18 = mat4(debugline_vs_params[0], debugline_vs_params[1], debugline_vs_params[2], debugline_vs_params[3]); + vec4 _28 = _18 * vec4(a_pos_a, 1.0); + vec4 _38 = _18 * vec4(a_pos_b, 1.0); + vec2 _78 = ((_38.xy / vec2(max(_38.w, 9.9999997473787516355514526367188e-05))) * debugline_vs_params[4].xy) - ((_28.xy / vec2(max(_28.w, 9.9999997473787516355514526367188e-05))) * debugline_vs_params[4].xy); + float _81 = length(_78); + vec2 _86; + if (_81 > 9.9999997473787516355514526367188e-05) + { + _86 = _78 / vec2(_81); + } + else + { + _86 = vec2(1.0, 0.0); + } + vec4 _115 = mix(_28, _38, vec4(a_corner.y)); + gl_Position = _115 + vec4((((vec2(-_86.y, _86.x) * a_corner.x) * ((a_width * 0.5) + 0.5)) / debugline_vs_params[4].xy) * _115.w, 0.0, 0.0); v_col = a_col; } @@ -107,23 +198,72 @@ vs_debugline_source_glsl300es := u8.[ 0x23,0x76,0x65,0x72,0x73,0x69,0x6f,0x6e,0x20,0x33,0x30,0x30,0x20,0x65,0x73,0x0a, 0x0a,0x75,0x6e,0x69,0x66,0x6f,0x72,0x6d,0x20,0x76,0x65,0x63,0x34,0x20,0x64,0x65, 0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70,0x61,0x72,0x61,0x6d, - 0x73,0x5b,0x34,0x5d,0x3b,0x0a,0x6c,0x61,0x79,0x6f,0x75,0x74,0x28,0x6c,0x6f,0x63, - 0x61,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x30,0x29,0x20,0x69,0x6e,0x20,0x76,0x65, - 0x63,0x33,0x20,0x61,0x5f,0x70,0x6f,0x73,0x3b,0x0a,0x6f,0x75,0x74,0x20,0x76,0x65, - 0x63,0x34,0x20,0x76,0x5f,0x63,0x6f,0x6c,0x3b,0x0a,0x6c,0x61,0x79,0x6f,0x75,0x74, - 0x28,0x6c,0x6f,0x63,0x61,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x31,0x29,0x20,0x69, - 0x6e,0x20,0x76,0x65,0x63,0x34,0x20,0x61,0x5f,0x63,0x6f,0x6c,0x3b,0x0a,0x0a,0x76, - 0x6f,0x69,0x64,0x20,0x6d,0x61,0x69,0x6e,0x28,0x29,0x0a,0x7b,0x0a,0x20,0x20,0x20, - 0x20,0x67,0x6c,0x5f,0x50,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x6d, - 0x61,0x74,0x34,0x28,0x64,0x65,0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73, - 0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x5b,0x30,0x5d,0x2c,0x20,0x64,0x65,0x62,0x75, - 0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x5b, - 0x31,0x5d,0x2c,0x20,0x64,0x65,0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73, - 0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x5b,0x32,0x5d,0x2c,0x20,0x64,0x65,0x62,0x75, - 0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x5b, - 0x33,0x5d,0x29,0x20,0x2a,0x20,0x76,0x65,0x63,0x34,0x28,0x61,0x5f,0x70,0x6f,0x73, - 0x2c,0x20,0x31,0x2e,0x30,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x76,0x5f,0x63,0x6f, - 0x6c,0x20,0x3d,0x20,0x61,0x5f,0x63,0x6f,0x6c,0x3b,0x0a,0x7d,0x0a,0x0a,0x00, + 0x73,0x5b,0x35,0x5d,0x3b,0x0a,0x6c,0x61,0x79,0x6f,0x75,0x74,0x28,0x6c,0x6f,0x63, + 0x61,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x31,0x29,0x20,0x69,0x6e,0x20,0x76,0x65, + 0x63,0x33,0x20,0x61,0x5f,0x70,0x6f,0x73,0x5f,0x61,0x3b,0x0a,0x6c,0x61,0x79,0x6f, + 0x75,0x74,0x28,0x6c,0x6f,0x63,0x61,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x32,0x29, + 0x20,0x69,0x6e,0x20,0x76,0x65,0x63,0x33,0x20,0x61,0x5f,0x70,0x6f,0x73,0x5f,0x62, + 0x3b,0x0a,0x6c,0x61,0x79,0x6f,0x75,0x74,0x28,0x6c,0x6f,0x63,0x61,0x74,0x69,0x6f, + 0x6e,0x20,0x3d,0x20,0x30,0x29,0x20,0x69,0x6e,0x20,0x76,0x65,0x63,0x32,0x20,0x61, + 0x5f,0x63,0x6f,0x72,0x6e,0x65,0x72,0x3b,0x0a,0x6c,0x61,0x79,0x6f,0x75,0x74,0x28, + 0x6c,0x6f,0x63,0x61,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x34,0x29,0x20,0x69,0x6e, + 0x20,0x66,0x6c,0x6f,0x61,0x74,0x20,0x61,0x5f,0x77,0x69,0x64,0x74,0x68,0x3b,0x0a, + 0x6f,0x75,0x74,0x20,0x76,0x65,0x63,0x34,0x20,0x76,0x5f,0x63,0x6f,0x6c,0x3b,0x0a, + 0x6c,0x61,0x79,0x6f,0x75,0x74,0x28,0x6c,0x6f,0x63,0x61,0x74,0x69,0x6f,0x6e,0x20, + 0x3d,0x20,0x33,0x29,0x20,0x69,0x6e,0x20,0x76,0x65,0x63,0x34,0x20,0x61,0x5f,0x63, + 0x6f,0x6c,0x3b,0x0a,0x0a,0x76,0x6f,0x69,0x64,0x20,0x6d,0x61,0x69,0x6e,0x28,0x29, + 0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x6d,0x61,0x74,0x34,0x20,0x5f,0x31,0x38,0x20, + 0x3d,0x20,0x6d,0x61,0x74,0x34,0x28,0x64,0x65,0x62,0x75,0x67,0x6c,0x69,0x6e,0x65, + 0x5f,0x76,0x73,0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x5b,0x30,0x5d,0x2c,0x20,0x64, + 0x65,0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70,0x61,0x72,0x61, + 0x6d,0x73,0x5b,0x31,0x5d,0x2c,0x20,0x64,0x65,0x62,0x75,0x67,0x6c,0x69,0x6e,0x65, + 0x5f,0x76,0x73,0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x5b,0x32,0x5d,0x2c,0x20,0x64, + 0x65,0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70,0x61,0x72,0x61, + 0x6d,0x73,0x5b,0x33,0x5d,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x76,0x65,0x63,0x34, + 0x20,0x5f,0x32,0x38,0x20,0x3d,0x20,0x5f,0x31,0x38,0x20,0x2a,0x20,0x76,0x65,0x63, + 0x34,0x28,0x61,0x5f,0x70,0x6f,0x73,0x5f,0x61,0x2c,0x20,0x31,0x2e,0x30,0x29,0x3b, + 0x0a,0x20,0x20,0x20,0x20,0x76,0x65,0x63,0x34,0x20,0x5f,0x33,0x38,0x20,0x3d,0x20, + 0x5f,0x31,0x38,0x20,0x2a,0x20,0x76,0x65,0x63,0x34,0x28,0x61,0x5f,0x70,0x6f,0x73, + 0x5f,0x62,0x2c,0x20,0x31,0x2e,0x30,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x76,0x65, + 0x63,0x32,0x20,0x5f,0x37,0x38,0x20,0x3d,0x20,0x28,0x28,0x5f,0x33,0x38,0x2e,0x78, + 0x79,0x20,0x2f,0x20,0x76,0x65,0x63,0x32,0x28,0x6d,0x61,0x78,0x28,0x5f,0x33,0x38, + 0x2e,0x77,0x2c,0x20,0x39,0x2e,0x39,0x39,0x39,0x39,0x39,0x39,0x37,0x34,0x37,0x33, + 0x37,0x38,0x37,0x35,0x31,0x36,0x33,0x35,0x35,0x35,0x31,0x34,0x35,0x32,0x36,0x33, + 0x36,0x37,0x31,0x38,0x38,0x65,0x2d,0x30,0x35,0x29,0x29,0x29,0x20,0x2a,0x20,0x64, + 0x65,0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70,0x61,0x72,0x61, + 0x6d,0x73,0x5b,0x34,0x5d,0x2e,0x78,0x79,0x29,0x20,0x2d,0x20,0x28,0x28,0x5f,0x32, + 0x38,0x2e,0x78,0x79,0x20,0x2f,0x20,0x76,0x65,0x63,0x32,0x28,0x6d,0x61,0x78,0x28, + 0x5f,0x32,0x38,0x2e,0x77,0x2c,0x20,0x39,0x2e,0x39,0x39,0x39,0x39,0x39,0x39,0x37, + 0x34,0x37,0x33,0x37,0x38,0x37,0x35,0x31,0x36,0x33,0x35,0x35,0x35,0x31,0x34,0x35, + 0x32,0x36,0x33,0x36,0x37,0x31,0x38,0x38,0x65,0x2d,0x30,0x35,0x29,0x29,0x29,0x20, + 0x2a,0x20,0x64,0x65,0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70, + 0x61,0x72,0x61,0x6d,0x73,0x5b,0x34,0x5d,0x2e,0x78,0x79,0x29,0x3b,0x0a,0x20,0x20, + 0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x20,0x5f,0x38,0x31,0x20,0x3d,0x20,0x6c,0x65, + 0x6e,0x67,0x74,0x68,0x28,0x5f,0x37,0x38,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x76, + 0x65,0x63,0x32,0x20,0x5f,0x38,0x36,0x3b,0x0a,0x20,0x20,0x20,0x20,0x69,0x66,0x20, + 0x28,0x5f,0x38,0x31,0x20,0x3e,0x20,0x39,0x2e,0x39,0x39,0x39,0x39,0x39,0x39,0x37, + 0x34,0x37,0x33,0x37,0x38,0x37,0x35,0x31,0x36,0x33,0x35,0x35,0x35,0x31,0x34,0x35, + 0x32,0x36,0x33,0x36,0x37,0x31,0x38,0x38,0x65,0x2d,0x30,0x35,0x29,0x0a,0x20,0x20, + 0x20,0x20,0x7b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x5f,0x38,0x36,0x20, + 0x3d,0x20,0x5f,0x37,0x38,0x20,0x2f,0x20,0x76,0x65,0x63,0x32,0x28,0x5f,0x38,0x31, + 0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x7d,0x0a,0x20,0x20,0x20,0x20,0x65,0x6c,0x73, + 0x65,0x0a,0x20,0x20,0x20,0x20,0x7b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20, + 0x5f,0x38,0x36,0x20,0x3d,0x20,0x76,0x65,0x63,0x32,0x28,0x31,0x2e,0x30,0x2c,0x20, + 0x30,0x2e,0x30,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x7d,0x0a,0x20,0x20,0x20,0x20, + 0x76,0x65,0x63,0x34,0x20,0x5f,0x31,0x31,0x35,0x20,0x3d,0x20,0x6d,0x69,0x78,0x28, + 0x5f,0x32,0x38,0x2c,0x20,0x5f,0x33,0x38,0x2c,0x20,0x76,0x65,0x63,0x34,0x28,0x61, + 0x5f,0x63,0x6f,0x72,0x6e,0x65,0x72,0x2e,0x79,0x29,0x29,0x3b,0x0a,0x20,0x20,0x20, + 0x20,0x67,0x6c,0x5f,0x50,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x5f, + 0x31,0x31,0x35,0x20,0x2b,0x20,0x76,0x65,0x63,0x34,0x28,0x28,0x28,0x28,0x76,0x65, + 0x63,0x32,0x28,0x2d,0x5f,0x38,0x36,0x2e,0x79,0x2c,0x20,0x5f,0x38,0x36,0x2e,0x78, + 0x29,0x20,0x2a,0x20,0x61,0x5f,0x63,0x6f,0x72,0x6e,0x65,0x72,0x2e,0x78,0x29,0x20, + 0x2a,0x20,0x28,0x28,0x61,0x5f,0x77,0x69,0x64,0x74,0x68,0x20,0x2a,0x20,0x30,0x2e, + 0x35,0x29,0x20,0x2b,0x20,0x30,0x2e,0x35,0x29,0x29,0x20,0x2f,0x20,0x64,0x65,0x62, + 0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70,0x61,0x72,0x61,0x6d,0x73, + 0x5b,0x34,0x5d,0x2e,0x78,0x79,0x29,0x20,0x2a,0x20,0x5f,0x31,0x31,0x35,0x2e,0x77, + 0x2c,0x20,0x30,0x2e,0x30,0x2c,0x20,0x30,0x2e,0x30,0x29,0x3b,0x0a,0x20,0x20,0x20, + 0x20,0x76,0x5f,0x63,0x6f,0x6c,0x20,0x3d,0x20,0x61,0x5f,0x63,0x6f,0x6c,0x3b,0x0a, + 0x7d,0x0a,0x0a,0x00, ]; /* #version 300 es @@ -161,6 +301,7 @@ fs_debugline_source_glsl300es := u8.[ struct debugline_vs_params { float4x4 mvp; + float4 viewport; }; struct main0_out @@ -171,14 +312,31 @@ fs_debugline_source_glsl300es := u8.[ struct main0_in { - float3 a_pos [[attribute(0)]]; - float4 a_col [[attribute(1)]]; + float2 a_corner [[attribute(0)]]; + float3 a_pos_a [[attribute(1)]]; + float3 a_pos_b [[attribute(2)]]; + float4 a_col [[attribute(3)]]; + float a_width [[attribute(4)]]; }; - vertex main0_out main0(main0_in in [[stage_in]], constant debugline_vs_params& _19 [[buffer(0)]]) + vertex main0_out main0(main0_in in [[stage_in]], constant debugline_vs_params& _13 [[buffer(0)]]) { main0_out out = {}; - out.gl_Position = _19.mvp * float4(in.a_pos, 1.0); + float4 _28 = _13.mvp * float4(in.a_pos_a, 1.0); + float4 _38 = _13.mvp * float4(in.a_pos_b, 1.0); + float2 _78 = ((_38.xy / float2(fast::max(_38.w, 9.9999997473787516355514526367188e-05))) * _13.viewport.xy) - ((_28.xy / float2(fast::max(_28.w, 9.9999997473787516355514526367188e-05))) * _13.viewport.xy); + float _81 = length(_78); + float2 _86; + if (_81 > 9.9999997473787516355514526367188e-05) + { + _86 = _78 / float2(_81); + } + else + { + _86 = float2(1.0, 0.0); + } + float4 _115 = mix(_28, _38, float4(in.a_corner.y)); + out.gl_Position = _115 + float4((((float2(-_86.y, _86.x) * in.a_corner.x) * ((in.a_width * 0.5) + 0.5)) / _13.viewport.xy) * _115.w, 0.0, 0.0); out.v_col = in.a_col; return out; } @@ -192,33 +350,82 @@ vs_debugline_source_metal_macos := u8.[ 0x6d,0x65,0x74,0x61,0x6c,0x3b,0x0a,0x0a,0x73,0x74,0x72,0x75,0x63,0x74,0x20,0x64, 0x65,0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70,0x61,0x72,0x61, 0x6d,0x73,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x78, - 0x34,0x20,0x6d,0x76,0x70,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,0x73,0x74,0x72,0x75,0x63, - 0x74,0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x6f,0x75,0x74,0x0a,0x7b,0x0a,0x20,0x20, - 0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x20,0x76,0x5f,0x63,0x6f,0x6c,0x20,0x5b, - 0x5b,0x75,0x73,0x65,0x72,0x28,0x6c,0x6f,0x63,0x6e,0x30,0x29,0x5d,0x5d,0x3b,0x0a, - 0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x20,0x67,0x6c,0x5f,0x50,0x6f, - 0x73,0x69,0x74,0x69,0x6f,0x6e,0x20,0x5b,0x5b,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f, - 0x6e,0x5d,0x5d,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,0x73,0x74,0x72,0x75,0x63,0x74,0x20, - 0x6d,0x61,0x69,0x6e,0x30,0x5f,0x69,0x6e,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x66, - 0x6c,0x6f,0x61,0x74,0x33,0x20,0x61,0x5f,0x70,0x6f,0x73,0x20,0x5b,0x5b,0x61,0x74, - 0x74,0x72,0x69,0x62,0x75,0x74,0x65,0x28,0x30,0x29,0x5d,0x5d,0x3b,0x0a,0x20,0x20, - 0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x20,0x61,0x5f,0x63,0x6f,0x6c,0x20,0x5b, - 0x5b,0x61,0x74,0x74,0x72,0x69,0x62,0x75,0x74,0x65,0x28,0x31,0x29,0x5d,0x5d,0x3b, - 0x0a,0x7d,0x3b,0x0a,0x0a,0x76,0x65,0x72,0x74,0x65,0x78,0x20,0x6d,0x61,0x69,0x6e, - 0x30,0x5f,0x6f,0x75,0x74,0x20,0x6d,0x61,0x69,0x6e,0x30,0x28,0x6d,0x61,0x69,0x6e, - 0x30,0x5f,0x69,0x6e,0x20,0x69,0x6e,0x20,0x5b,0x5b,0x73,0x74,0x61,0x67,0x65,0x5f, - 0x69,0x6e,0x5d,0x5d,0x2c,0x20,0x63,0x6f,0x6e,0x73,0x74,0x61,0x6e,0x74,0x20,0x64, - 0x65,0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76,0x73,0x5f,0x70,0x61,0x72,0x61, - 0x6d,0x73,0x26,0x20,0x5f,0x31,0x39,0x20,0x5b,0x5b,0x62,0x75,0x66,0x66,0x65,0x72, - 0x28,0x30,0x29,0x5d,0x5d,0x29,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x6d,0x61,0x69, - 0x6e,0x30,0x5f,0x6f,0x75,0x74,0x20,0x6f,0x75,0x74,0x20,0x3d,0x20,0x7b,0x7d,0x3b, - 0x0a,0x20,0x20,0x20,0x20,0x6f,0x75,0x74,0x2e,0x67,0x6c,0x5f,0x50,0x6f,0x73,0x69, - 0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x5f,0x31,0x39,0x2e,0x6d,0x76,0x70,0x20,0x2a, - 0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x28,0x69,0x6e,0x2e,0x61,0x5f,0x70,0x6f,0x73, - 0x2c,0x20,0x31,0x2e,0x30,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x6f,0x75,0x74,0x2e, - 0x76,0x5f,0x63,0x6f,0x6c,0x20,0x3d,0x20,0x69,0x6e,0x2e,0x61,0x5f,0x63,0x6f,0x6c, - 0x3b,0x0a,0x20,0x20,0x20,0x20,0x72,0x65,0x74,0x75,0x72,0x6e,0x20,0x6f,0x75,0x74, - 0x3b,0x0a,0x7d,0x0a,0x0a,0x00, + 0x34,0x20,0x6d,0x76,0x70,0x3b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74, + 0x34,0x20,0x76,0x69,0x65,0x77,0x70,0x6f,0x72,0x74,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a, + 0x73,0x74,0x72,0x75,0x63,0x74,0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x6f,0x75,0x74, + 0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x20,0x76,0x5f, + 0x63,0x6f,0x6c,0x20,0x5b,0x5b,0x75,0x73,0x65,0x72,0x28,0x6c,0x6f,0x63,0x6e,0x30, + 0x29,0x5d,0x5d,0x3b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x20, + 0x67,0x6c,0x5f,0x50,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x20,0x5b,0x5b,0x70,0x6f, + 0x73,0x69,0x74,0x69,0x6f,0x6e,0x5d,0x5d,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,0x73,0x74, + 0x72,0x75,0x63,0x74,0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x69,0x6e,0x0a,0x7b,0x0a, + 0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,0x61,0x5f,0x63,0x6f,0x72, + 0x6e,0x65,0x72,0x20,0x5b,0x5b,0x61,0x74,0x74,0x72,0x69,0x62,0x75,0x74,0x65,0x28, + 0x30,0x29,0x5d,0x5d,0x3b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x33, + 0x20,0x61,0x5f,0x70,0x6f,0x73,0x5f,0x61,0x20,0x5b,0x5b,0x61,0x74,0x74,0x72,0x69, + 0x62,0x75,0x74,0x65,0x28,0x31,0x29,0x5d,0x5d,0x3b,0x0a,0x20,0x20,0x20,0x20,0x66, + 0x6c,0x6f,0x61,0x74,0x33,0x20,0x61,0x5f,0x70,0x6f,0x73,0x5f,0x62,0x20,0x5b,0x5b, + 0x61,0x74,0x74,0x72,0x69,0x62,0x75,0x74,0x65,0x28,0x32,0x29,0x5d,0x5d,0x3b,0x0a, + 0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x20,0x61,0x5f,0x63,0x6f,0x6c, + 0x20,0x5b,0x5b,0x61,0x74,0x74,0x72,0x69,0x62,0x75,0x74,0x65,0x28,0x33,0x29,0x5d, + 0x5d,0x3b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x20,0x61,0x5f,0x77, + 0x69,0x64,0x74,0x68,0x20,0x5b,0x5b,0x61,0x74,0x74,0x72,0x69,0x62,0x75,0x74,0x65, + 0x28,0x34,0x29,0x5d,0x5d,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,0x76,0x65,0x72,0x74,0x65, + 0x78,0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x6f,0x75,0x74,0x20,0x6d,0x61,0x69,0x6e, + 0x30,0x28,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x69,0x6e,0x20,0x69,0x6e,0x20,0x5b,0x5b, + 0x73,0x74,0x61,0x67,0x65,0x5f,0x69,0x6e,0x5d,0x5d,0x2c,0x20,0x63,0x6f,0x6e,0x73, + 0x74,0x61,0x6e,0x74,0x20,0x64,0x65,0x62,0x75,0x67,0x6c,0x69,0x6e,0x65,0x5f,0x76, + 0x73,0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x26,0x20,0x5f,0x31,0x33,0x20,0x5b,0x5b, + 0x62,0x75,0x66,0x66,0x65,0x72,0x28,0x30,0x29,0x5d,0x5d,0x29,0x0a,0x7b,0x0a,0x20, + 0x20,0x20,0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x6f,0x75,0x74,0x20,0x6f,0x75,0x74, + 0x20,0x3d,0x20,0x7b,0x7d,0x3b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74, + 0x34,0x20,0x5f,0x32,0x38,0x20,0x3d,0x20,0x5f,0x31,0x33,0x2e,0x6d,0x76,0x70,0x20, + 0x2a,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x28,0x69,0x6e,0x2e,0x61,0x5f,0x70,0x6f, + 0x73,0x5f,0x61,0x2c,0x20,0x31,0x2e,0x30,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x66, + 0x6c,0x6f,0x61,0x74,0x34,0x20,0x5f,0x33,0x38,0x20,0x3d,0x20,0x5f,0x31,0x33,0x2e, + 0x6d,0x76,0x70,0x20,0x2a,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x28,0x69,0x6e,0x2e, + 0x61,0x5f,0x70,0x6f,0x73,0x5f,0x62,0x2c,0x20,0x31,0x2e,0x30,0x29,0x3b,0x0a,0x20, + 0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,0x5f,0x37,0x38,0x20,0x3d,0x20, + 0x28,0x28,0x5f,0x33,0x38,0x2e,0x78,0x79,0x20,0x2f,0x20,0x66,0x6c,0x6f,0x61,0x74, + 0x32,0x28,0x66,0x61,0x73,0x74,0x3a,0x3a,0x6d,0x61,0x78,0x28,0x5f,0x33,0x38,0x2e, + 0x77,0x2c,0x20,0x39,0x2e,0x39,0x39,0x39,0x39,0x39,0x39,0x37,0x34,0x37,0x33,0x37, + 0x38,0x37,0x35,0x31,0x36,0x33,0x35,0x35,0x35,0x31,0x34,0x35,0x32,0x36,0x33,0x36, + 0x37,0x31,0x38,0x38,0x65,0x2d,0x30,0x35,0x29,0x29,0x29,0x20,0x2a,0x20,0x5f,0x31, + 0x33,0x2e,0x76,0x69,0x65,0x77,0x70,0x6f,0x72,0x74,0x2e,0x78,0x79,0x29,0x20,0x2d, + 0x20,0x28,0x28,0x5f,0x32,0x38,0x2e,0x78,0x79,0x20,0x2f,0x20,0x66,0x6c,0x6f,0x61, + 0x74,0x32,0x28,0x66,0x61,0x73,0x74,0x3a,0x3a,0x6d,0x61,0x78,0x28,0x5f,0x32,0x38, + 0x2e,0x77,0x2c,0x20,0x39,0x2e,0x39,0x39,0x39,0x39,0x39,0x39,0x37,0x34,0x37,0x33, + 0x37,0x38,0x37,0x35,0x31,0x36,0x33,0x35,0x35,0x35,0x31,0x34,0x35,0x32,0x36,0x33, + 0x36,0x37,0x31,0x38,0x38,0x65,0x2d,0x30,0x35,0x29,0x29,0x29,0x20,0x2a,0x20,0x5f, + 0x31,0x33,0x2e,0x76,0x69,0x65,0x77,0x70,0x6f,0x72,0x74,0x2e,0x78,0x79,0x29,0x3b, + 0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x20,0x5f,0x38,0x31,0x20,0x3d, + 0x20,0x6c,0x65,0x6e,0x67,0x74,0x68,0x28,0x5f,0x37,0x38,0x29,0x3b,0x0a,0x20,0x20, + 0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,0x5f,0x38,0x36,0x3b,0x0a,0x20,0x20, + 0x20,0x20,0x69,0x66,0x20,0x28,0x5f,0x38,0x31,0x20,0x3e,0x20,0x39,0x2e,0x39,0x39, + 0x39,0x39,0x39,0x39,0x37,0x34,0x37,0x33,0x37,0x38,0x37,0x35,0x31,0x36,0x33,0x35, + 0x35,0x35,0x31,0x34,0x35,0x32,0x36,0x33,0x36,0x37,0x31,0x38,0x38,0x65,0x2d,0x30, + 0x35,0x29,0x0a,0x20,0x20,0x20,0x20,0x7b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20, + 0x20,0x5f,0x38,0x36,0x20,0x3d,0x20,0x5f,0x37,0x38,0x20,0x2f,0x20,0x66,0x6c,0x6f, + 0x61,0x74,0x32,0x28,0x5f,0x38,0x31,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x7d,0x0a, + 0x20,0x20,0x20,0x20,0x65,0x6c,0x73,0x65,0x0a,0x20,0x20,0x20,0x20,0x7b,0x0a,0x20, + 0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x5f,0x38,0x36,0x20,0x3d,0x20,0x66,0x6c,0x6f, + 0x61,0x74,0x32,0x28,0x31,0x2e,0x30,0x2c,0x20,0x30,0x2e,0x30,0x29,0x3b,0x0a,0x20, + 0x20,0x20,0x20,0x7d,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x20, + 0x5f,0x31,0x31,0x35,0x20,0x3d,0x20,0x6d,0x69,0x78,0x28,0x5f,0x32,0x38,0x2c,0x20, + 0x5f,0x33,0x38,0x2c,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x28,0x69,0x6e,0x2e,0x61, + 0x5f,0x63,0x6f,0x72,0x6e,0x65,0x72,0x2e,0x79,0x29,0x29,0x3b,0x0a,0x20,0x20,0x20, + 0x20,0x6f,0x75,0x74,0x2e,0x67,0x6c,0x5f,0x50,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e, + 0x20,0x3d,0x20,0x5f,0x31,0x31,0x35,0x20,0x2b,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34, + 0x28,0x28,0x28,0x28,0x66,0x6c,0x6f,0x61,0x74,0x32,0x28,0x2d,0x5f,0x38,0x36,0x2e, + 0x79,0x2c,0x20,0x5f,0x38,0x36,0x2e,0x78,0x29,0x20,0x2a,0x20,0x69,0x6e,0x2e,0x61, + 0x5f,0x63,0x6f,0x72,0x6e,0x65,0x72,0x2e,0x78,0x29,0x20,0x2a,0x20,0x28,0x28,0x69, + 0x6e,0x2e,0x61,0x5f,0x77,0x69,0x64,0x74,0x68,0x20,0x2a,0x20,0x30,0x2e,0x35,0x29, + 0x20,0x2b,0x20,0x30,0x2e,0x35,0x29,0x29,0x20,0x2f,0x20,0x5f,0x31,0x33,0x2e,0x76, + 0x69,0x65,0x77,0x70,0x6f,0x72,0x74,0x2e,0x78,0x79,0x29,0x20,0x2a,0x20,0x5f,0x31, + 0x31,0x35,0x2e,0x77,0x2c,0x20,0x30,0x2e,0x30,0x2c,0x20,0x30,0x2e,0x30,0x29,0x3b, + 0x0a,0x20,0x20,0x20,0x20,0x6f,0x75,0x74,0x2e,0x76,0x5f,0x63,0x6f,0x6c,0x20,0x3d, + 0x20,0x69,0x6e,0x2e,0x61,0x5f,0x63,0x6f,0x6c,0x3b,0x0a,0x20,0x20,0x20,0x20,0x72, + 0x65,0x74,0x75,0x72,0x6e,0x20,0x6f,0x75,0x74,0x3b,0x0a,0x7d,0x0a,0x0a,0x00, ]; /* #include @@ -276,14 +483,20 @@ debugline_shader_desc :: (backend: sg_backend) -> sg_shader_desc { desc.fragment_func.source = xx *fs_debugline_source_glsl430; desc.fragment_func.entry = "main"; desc.attrs[0].base_type = .FLOAT; - desc.attrs[0].glsl_name = "a_pos"; + desc.attrs[0].glsl_name = "a_corner"; desc.attrs[1].base_type = .FLOAT; - desc.attrs[1].glsl_name = "a_col"; + desc.attrs[1].glsl_name = "a_pos_a"; + desc.attrs[2].base_type = .FLOAT; + desc.attrs[2].glsl_name = "a_pos_b"; + desc.attrs[3].base_type = .FLOAT; + desc.attrs[3].glsl_name = "a_col"; + desc.attrs[4].base_type = .FLOAT; + desc.attrs[4].glsl_name = "a_width"; desc.uniform_blocks[0].stage = .VERTEX; desc.uniform_blocks[0].layout = .STD140; - desc.uniform_blocks[0].size = 64; + desc.uniform_blocks[0].size = 80; desc.uniform_blocks[0].glsl_uniforms[0].type = .FLOAT4; - desc.uniform_blocks[0].glsl_uniforms[0].array_count = 4; + desc.uniform_blocks[0].glsl_uniforms[0].array_count = 5; desc.uniform_blocks[0].glsl_uniforms[0].glsl_name = "debugline_vs_params"; case .GLES3; desc.vertex_func.source = xx *vs_debugline_source_glsl300es; @@ -291,14 +504,20 @@ debugline_shader_desc :: (backend: sg_backend) -> sg_shader_desc { desc.fragment_func.source = xx *fs_debugline_source_glsl300es; desc.fragment_func.entry = "main"; desc.attrs[0].base_type = .FLOAT; - desc.attrs[0].glsl_name = "a_pos"; + desc.attrs[0].glsl_name = "a_corner"; desc.attrs[1].base_type = .FLOAT; - desc.attrs[1].glsl_name = "a_col"; + desc.attrs[1].glsl_name = "a_pos_a"; + desc.attrs[2].base_type = .FLOAT; + desc.attrs[2].glsl_name = "a_pos_b"; + desc.attrs[3].base_type = .FLOAT; + desc.attrs[3].glsl_name = "a_col"; + desc.attrs[4].base_type = .FLOAT; + desc.attrs[4].glsl_name = "a_width"; desc.uniform_blocks[0].stage = .VERTEX; desc.uniform_blocks[0].layout = .STD140; - desc.uniform_blocks[0].size = 64; + desc.uniform_blocks[0].size = 80; desc.uniform_blocks[0].glsl_uniforms[0].type = .FLOAT4; - desc.uniform_blocks[0].glsl_uniforms[0].array_count = 4; + desc.uniform_blocks[0].glsl_uniforms[0].array_count = 5; desc.uniform_blocks[0].glsl_uniforms[0].glsl_name = "debugline_vs_params"; case .METAL_MACOS; desc.vertex_func.source = xx *vs_debugline_source_metal_macos; @@ -307,9 +526,12 @@ debugline_shader_desc :: (backend: sg_backend) -> sg_shader_desc { desc.fragment_func.entry = "main0"; desc.attrs[0].base_type = .FLOAT; desc.attrs[1].base_type = .FLOAT; + desc.attrs[2].base_type = .FLOAT; + desc.attrs[3].base_type = .FLOAT; + desc.attrs[4].base_type = .FLOAT; desc.uniform_blocks[0].stage = .VERTEX; desc.uniform_blocks[0].layout = .STD140; - desc.uniform_blocks[0].size = 64; + desc.uniform_blocks[0].size = 80; desc.uniform_blocks[0].msl_buffer_n = 0; } return desc; diff --git a/src/shaders/shader_debugline.glsl b/src/shaders/shader_debugline.glsl index d6c04aa..a2dd04b 100644 --- a/src/shaders/shader_debugline.glsl +++ b/src/shaders/shader_debugline.glsl @@ -1,16 +1,44 @@ @vs vs_debugline -in vec3 a_pos; +// Lines are drawn as camera-facing quads rather than GL lines, so they can have +// a real pixel width. Buffer 0 holds the six corners of a unit quad, buffer 1 +// one instance per line. +in vec2 a_corner; // x = which side (-1 / +1), y = which end (0 = a, 1 = b) +in vec3 a_pos_a; +in vec3 a_pos_b; in vec4 a_col; +in float a_width; // in pixels layout(binding=0) uniform debugline_vs_params { mat4 mvp; + vec4 viewport; // xy = size in pixels, zw = unused }; out vec4 v_col; void main() { - gl_Position = mvp * vec4(a_pos, 1.0); + vec4 clip_a = mvp * vec4(a_pos_a, 1.0); + vec4 clip_b = mvp * vec4(a_pos_b, 1.0); + + // Widen in screen space so the line keeps its pixel width at any depth. + // Guard against w <= 0: an endpoint behind the eye has no screen position, + // so fall back to the other end's and let clipping handle the rest. + float wa = max(clip_a.w, 0.0001); + float wb = max(clip_b.w, 0.0001); + vec2 screen_a = (clip_a.xy / wa) * viewport.xy; + vec2 screen_b = (clip_b.xy / wb) * viewport.xy; + + vec2 delta = screen_b - screen_a; + float len = length(delta); + vec2 dir = (len > 0.0001) ? delta / len : vec2(1.0, 0.0); + vec2 normal = vec2(-dir.y, dir.x); + + vec4 clip = mix(clip_a, clip_b, a_corner.y); + // Half a pixel of extra width keeps thin lines from disappearing between + // sample points. + vec2 offset = normal * a_corner.x * (a_width * 0.5 + 0.5) / viewport.xy; + gl_Position = clip + vec4(offset * clip.w, 0.0, 0.0); + v_col = a_col; } diff --git a/src/tests/exe_tests/runner.jai b/src/tests/exe_tests/runner.jai index 3019e95..db4cca1 100644 --- a/src/tests/exe_tests/runner.jai +++ b/src/tests/exe_tests/runner.jai @@ -1,15 +1,39 @@ +// A single test gets this long to run its commands before it is declared hung +// and the run moves on. This only fires while frames are still being produced; +// a genuine freeze has to be caught by a timeout around the process itself, +// which is what run_tests.sh does. +EXE_TEST_TIMEOUT_SECONDS :: 30.0; + Exe_Runner :: struct { - suite_idx : int; - test_idx : int; - cmd_idx : int; - wait_until : float64; - test_failed : bool; - test_started : bool; - done : bool; + suite_idx : int; + test_idx : int; + cmd_idx : int; + wait_until : float64; + test_failed : bool; + test_started : bool; + done : bool; + passed : int; + failed : int; + test_deadline : float64; } g_exe_runner : Exe_Runner; +// Ends the process with a status that reflects the run, so a failing exe test +// actually fails the build instead of quitting quietly with 0. +finish_exe_tests :: (r: *Exe_Runner) { + total := r.passed + r.failed; + if r.failed == 0 { + print("[exe tests] All % test(s) passed.\n", total); + } else { + print("[exe tests] %/% passed, % FAILED.\n", r.passed, total, r.failed); + } + r.done = true; + sapp_request_quit(); + status : s32 = ifx r.failed > 0 then cast(s32) 1 else cast(s32) 0; + exit(status); +} + run_exe_tests :: () { r := *g_exe_runner; if r.done then return; @@ -20,8 +44,7 @@ run_exe_tests :: () { if r.suite_idx >= g_test_runner_state.count { print("[exe tests] All suites complete.\n"); - r.done = true; - sapp_request_quit(); + finish_exe_tests(r); return; } @@ -40,15 +63,30 @@ run_exe_tests :: () { if !r.test_started { print("[exe tests] Starting '%' / '%'\n", suite.name, test.name); - r.test_started = true; + r.test_started = true; + r.test_deadline = get_time() + EXE_TEST_TIMEOUT_SECONDS; + } + + // A test that outlives its budget is failed where it stands rather than + // being allowed to stall the whole run. + if get_time() > r.test_deadline { + print("[exe tests] TIMEOUT '%' / '%' after %s (cmd % of %)\n", + suite.name, test.name, EXE_TEST_TIMEOUT_SECONDS, r.cmd_idx, test.cmds.count); + r.test_failed = true; + r.cmd_idx = test.cmds.count; } if r.cmd_idx >= test.cmds.count { if r.test_failed { + r.failed += 1; print("[exe tests] FAIL '%' / '%'\n", suite.name, test.name); } else { + r.passed += 1; print("[exe tests] PASS '%' / '%'\n", suite.name, test.name); } + // Cleared here too: timing out mid-WAIT leaves a stale deadline behind, + // which would make the next test's first WAIT elapse instantly. + r.wait_until = 0; r.test_idx += 1; r.cmd_idx = 0; r.test_failed = false; diff --git a/src/tests/framework.jai b/src/tests/framework.jai new file mode 100644 index 0000000..8224df4 --- /dev/null +++ b/src/tests/framework.jai @@ -0,0 +1,12 @@ +// Test framework only — no test cases. This is loaded under every test flag so +// that the game side can write suites and exe tests exactly like the engine does. +// +// Unit tests need nothing beyond this: put them in a #if FLAG_TEST_GAME block +// next to the code they cover, with a local #run to call them, and a failing +// check turns into a compile error. +// +// Exe tests are registered from game_exe_tests_add(), which the engine calls +// during init when built with test_exe_game. + +#load "utils.jai"; +#load "exe_tests/index.jai"; diff --git a/src/tests/index.jai b/src/tests/index.jai index e2c863b..279eaaa 100644 --- a/src/tests/index.jai +++ b/src/tests/index.jai @@ -1,6 +1,7 @@ -#load "utils.jai"; +// Engine test cases. The framework itself lives in framework.jai, which is +// loaded separately so the game side gets it too. + #load "world_test.jai"; #load "../editor/rdm_disk_test.jai"; #load "engine_exe_tests/index.jai"; -#load "exe_tests/index.jai"; diff --git a/src/tests/world_test.jai b/src/tests/world_test.jai index 5ea94fd..a2bf76c 100644 --- a/src/tests/world_test.jai +++ b/src/tests/world_test.jai @@ -268,11 +268,106 @@ test_legacy_load_cursor_fix :: () { end_suite(s); } -#run { +// Entity types come from the game, so these run against whatever the game +// declares first rather than naming a type the engine cannot know about. +test_entity_save_load_roundtrip :: () { + s := begin_suite("entity save/load roundtrip"); + if ENTITY_TYPE_TABLE.count == 0 { + end_suite(s); + return; + } + type_name := ENTITY_TYPE_TABLE[0].name; + + world := make_test_world(); + a := spawn_entity(*world, type_name, .{3.5, 1.0, -2.0}, 17); + b := spawn_entity(*world, type_name, .{0.0, 0.0, 0.0}); + check(*s, "spawning returns entities", a != null && b != null); + if a == null || b == null { + end_suite(s); + return; + } + check(*s, "spawned entities get distinct ids", a.id != b.id); + + saved_fields := entity_fields_to_strings(a); + + json_str, bin_data := save_world(*world); + bin_bytes: []u8; + bin_bytes.data = bin_data.data; + bin_bytes.count = bin_data.count; + + loaded, ok := load_world_from_json(json_str, bin_bytes); + check(*s, "load succeeds", ok); + check(*s, "both entities come back", loaded.entities.count == 2); + if loaded.entities.count == 2 { + la := loaded.entities[0]; + check(*s, "type survives", la.type == a.type); + check(*s, "id survives", la.id == a.id); + check(*s, "position survives", la.position == Vector3.{3.5, 1.0, -2.0}); + check(*s, "orientation survives", la.orientation == 17); + + loaded_fields := entity_fields_to_strings(la); + same := loaded_fields.count == saved_fields.count; + for i: 0..min(loaded_fields.count, saved_fields.count)-1 { + if loaded_fields[i].name != saved_fields[i].name then same = false; + if loaded_fields[i].value != saved_fields[i].value then same = false; + } + check(*s, "editable fields survive", same); + } + + // Handing out an id a loaded entity already owns would make two entities + // indistinguishable to anything that refers to them by id. + check(*s, "the next id clears every loaded one", loaded.next_entity_id > max(a.id, b.id)); + + // The rest of the world is untouched by the entities riding along with it. + check(*s, "notes still load", loaded.notes.count == 1); + check(*s, "emitters still load", loaded.emitter_instances.count == 1); + + end_suite(s); +} + +// Saves are name-keyed so that a build with fewer types or fields than the one +// that wrote them still loads. Both halves of that log and carry on. +test_entity_unknown_names :: () { + s := begin_suite("entities tolerate names from other builds"); + if ENTITY_TYPE_TABLE.count == 0 { + end_suite(s); + return; + } + + world := make_test_world(); + e := spawn_entity(*world, ENTITY_TYPE_TABLE[0].name, .{1.0, 2.0, 3.0}); + check(*s, "the known type spawns", e != null); + if e == null { + end_suite(s); + return; + } + + // Expect one logged error here: that is the reported half of "skipped". + gone := spawn_entity(*world, "A_Type_That_Went_Away", .{0.0, 0.0, 0.0}); + check(*s, "an unknown type spawns nothing", gone == null); + check(*s, "and adds nothing to the world", world.entities.count == 1); + + before := entity_fields_to_strings(e); + entity_apply_field(e, "a_field_that_went_away", "12"); + after := entity_fields_to_strings(e); + unchanged := before.count == after.count; + for i: 0..min(before.count, after.count)-1 { + if before[i].value != after[i].value then unchanged = false; + } + check(*s, "an unknown field name changes nothing", unchanged); + + end_suite(s); +} + +// Stallable: the entity tests read ENTITY_TYPE_TABLE, which cannot be built +// until the metaprogram has collected every @Entity in the program. +#run,stallable { test_floor_div_mod(); test_coord_roundtrip(); test_chunk_coord_values(); test_world_save_load_roundtrip(); test_world_json_chunk_offsets(); test_legacy_load_cursor_fix(); + test_entity_save_load_roundtrip(); + test_entity_unknown_names(); } diff --git a/src/ui/autoedit.jai b/src/ui/autoedit.jai index f35d5b8..324b68a 100644 --- a/src/ui/autoedit.jai +++ b/src/ui/autoedit.jai @@ -59,18 +59,59 @@ note_to_autoedit_conf :: (notes: []string) -> Autoedit_Conf { return .{}; } -input_code_from_type_and_notes :: (name: string, type: *Type_Info, notes: []string) -> string { +// Types we can generate an editor widget for. Anything else (nested structs, +// arrays, the '#as using base' of an entity, ...) is skipped entirely. +autoedit_supports_type :: (type: *Type_Info) -> bool { + return type == type_info(float) || type == type_info(s32) || type == type_info(bool) + || type == type_info(Vector3) || type == type_info(string); +} + +// Widgets that need a GetRect id get one per field. Two autoedit panels can be +// on screen at once (the world config and an entity's fields), so the id is +// namespaced by the caller's identifier to keep the two sets apart. +autoedit_widget_id :: (identifier: s32, field_index: int) -> s32 { + return identifier * 1000 + cast(s32) field_index; +} + +// A text input hands back a view into the widget's own buffer, so the value has +// to be copied out on every keystroke. The previous copy is freed only when this +// same helper allocated it and the field still points at it: a field's starting +// value is usually a literal from the struct definition, which must not be freed. +autoedit_owned_text : Table(*string, string); + +autoedit_set_text :: (field: *string, text: string) -> string { + found, previous := table_find(*autoedit_owned_text, field); + if found && previous.data == field.data free(previous); + result := copy_string(text); + table_set(*autoedit_owned_text, field, result); + return result; +} + +input_code_from_type_and_notes :: (name: string, type: *Type_Info, notes: []string, index: int) -> string { autoconf := note_to_autoedit_conf(notes); builder : String_Builder; + // Each field resets the row height, so a field that wants a taller widget can + // just set it and let the trailing advance pick it up. + print_to_builder(*builder, "r.h = ui_h(4,0);\n"); print_to_builder(*builder, "GR.label(r, \"%\", *t_label_left(theme));\n", name); print_to_builder(*builder, "r.y += r.h;\n"); - - if type == type_info(float) || type == type_info(s32) { + + if type == type_info(bool) { + print_to_builder(*builder, "if GR.button(r, ifx value.% then \"true\" else \"false\", *t_button_selectable(theme, value.%), autoedit_widget_id(identifier, %)) then value.% = !value.%;\n", + name, name, index, name, name); + } else if type == type_info(float) || type == type_info(s32) { if autoconf.kind == .SLIDER { print_to_builder(*builder, "GR.slider(r, *value.%, %, %, %, *theme.slider_theme);\n", name, autoconf.min, autoconf.max, autoconf.step); } else { print_to_builder(*builder, "GR.number_input(r, tprint(\"\%\", value.%), *value.%, %, %, *number_theme);\n", name, name, autoconf.min, autoconf.max); } + } else if type == type_info(string) { + // Roomier than the other widgets: text fields hold a sentence, not a number. + print_to_builder(*builder, "r.h = ui_h(7,0);\n"); + print_to_builder(*builder, "{\n"); + print_to_builder(*builder, "action, _, text_state := GR.text_input(r, value.%, *theme.text_input_theme, autoedit_widget_id(identifier, %));\n", name, index); + print_to_builder(*builder, "if (action & .TEXT_MODIFIED) || (action & .ENTERED) value.% = autoedit_set_text(*value.%, text_state.text);\n", name, name); + print_to_builder(*builder, "}\n"); } else if type == type_info(Vector3) { if autoconf.kind == .DEFAULT { print_to_builder(*builder, "{\n"); @@ -83,9 +124,9 @@ input_code_from_type_and_notes :: (name: string, type: *Type_Info, notes: []stri print_to_builder(*builder, "r.w = orig_w; r.x = orig_x;\n"); print_to_builder(*builder, "}\n"); } else if autoconf.kind == .COLOR { - print_to_builder(*builder, "if GR.button(r, \"Edit color\", *t_button_color(theme, .{value.%.x, value.%.y, value.%.z, 1.0})) then cur_edit_color = *value.%;\n", name, name, name, name); + print_to_builder(*builder, "if GR.button(r, \"Edit color\", *t_button_color(theme, .{value.%.x, value.%.y, value.%.z, 1.0}), autoedit_widget_id(identifier, %)) then cur_edit_color = *value.%;\n", name, name, name, index, name); } - + } print_to_builder(*builder, "r.y += r.h;\n"); return builder_to_string(*builder); @@ -106,7 +147,10 @@ autoedit :: (rect: GR.Rect, value: *$T, theme: *GR.Overall_Theme, identifier: s3 ti := type_info(T); #assert #run type_info(T).type == .STRUCT "Autoedit only works for structs"; for ti.members { - print_to_builder(*builder, "%\n", input_code_from_type_and_notes(it.name, it.type, it.notes)); + if it.flags & .CONSTANT continue; + if it.flags & .USING continue; // e.g. an entity's Entity base + if !autoedit_supports_type(it.type) continue; + print_to_builder(*builder, "%\n", input_code_from_type_and_notes(it.name, it.type, it.notes, it_index)); } return builder_to_string(*builder); } diff --git a/src/world.jai b/src/world.jai index b066399..bdb0507 100644 --- a/src/world.jai +++ b/src/world.jai @@ -91,6 +91,8 @@ World :: struct { chunks : Table(Chunk_Key, Chunk, chunk_key_hash, chunk_key_compare); emitter_instances : [..]Particle_Emitter_Instance; notes : [..]Editor_Note; + entities : [..]*Entity; + next_entity_id : u32; rdm_overrides : [..]Rdm_Instance_Override; rdm_lookup : [..]Rdm_Atlas_Entry; // populated by bake (Step 5) and by loader (Step 6) } @@ -207,6 +209,33 @@ lworld :: (name: string) { load_world(name); } @Command; +// Copy another world's config (sky, sun, water, ...) onto the loaded world, so a +// new level can start from an existing look instead of retyping the values. +copy_world_info :: (name: string) { + if !current_world.valid { + log_error("Cannot copy: no world loaded"); + return; + } + #if OS != .WASM { + file :: #import "File"; + path := tprint("%/worlds/%/world.json", GAME_RESOURCES_DIR, name); + json_str, read_ok := file.read_entire_file(path,, allocator = temp); + if !read_ok { + log_error("Cannot copy: failed to read '%'", path); + return; + } + + parse_ok, wj := Jaison.json_parse_string(json_str, World_Json,, temp); + if !parse_ok { + log_error("Cannot copy: failed to parse world '%'", name); + return; + } + + current_world.world.conf = world_config_from_json(*wj.config); + log_info("Copied world config from '%'", name); + } +} @Command + init_world_system :: () { Pool.set_allocators(*current_world.pool); } @@ -222,6 +251,10 @@ unload_current_world :: () { array_free(chunk.groups); } deinit(*current_world.world.chunks); + free_all_entities(*current_world.world); + // Entity ids restart per world, so a stale editor selection would latch onto + // an unrelated entity in the next one. + #if OS != .WASM { level_editor_clear_entity_selection(); } array_free(current_world.world.rdm_lookup); Pool.reset(*current_world.pool); current_world.valid = false; @@ -353,6 +386,11 @@ resolve_emitter_definitions :: (world: *World) { for *inst: world.emitter_instances { inst.definition = get_emitter_def(inst.definition_name); } + for e: world.entities { + for *inst: e.emitters { + inst.definition = get_emitter_def(inst.definition_name); + } + } } clear_world :: () { @@ -384,6 +422,7 @@ World_Json :: struct { chunks : [..]World_Json_Chunk; emitters : [..]World_Json_Emitter; notes : [..]World_Json_Note; + entities : [..]World_Json_Entity; rdm_overrides : [..]World_Json_Rdm_Override; } @@ -424,6 +463,17 @@ World_Json_Note :: struct { position : [3]s32; } +// Entity fields are stored as name/value string pairs rather than a typed +// object, so that adding, removing or renaming a field never breaks loading: +// unknown names are skipped and missing ones keep their struct defaults. +World_Json_Entity :: struct { + type : string; // entity type name, so reordering ENTITY_TYPES is safe + id : u32; + position : [3]float; + orientation : u8; // absent in worlds saved before entities could rotate + fields : [..]Entity_Field; +} + World_Json_Rdm_Override :: struct { x : s32; y : s32; @@ -619,7 +669,7 @@ save_world :: (world: *World) -> (json: string, chunks_bin: string) { } wj: World_Json; - wj.version = 4; + wj.version = 5; wj.name = world.name; wj.config = world_config_to_json(*world.conf); @@ -648,6 +698,16 @@ save_world :: (world: *World) -> (json: string, chunks_bin: string) { array_add(*wj.notes, jn); } + for e: world.entities { + je: World_Json_Entity; + je.type = entity_type_name(e.type); + je.id = e.id; + je.position = e.position.component; + je.orientation = e.orientation; + for f: entity_fields_to_strings(e) array_add(*je.fields, f); + array_add(*wj.entities, je); + } + for ov: world.rdm_overrides { size := ifx ov.rdm_size > 0 then ov.rdm_size else RDM_DEFAULT_SIZE; array_add(*wj.rdm_overrides, .{x=ov.x, y=ov.y, z=ov.z, rdm_enabled=ov.rdm_enabled, rdm_size=size}); @@ -735,6 +795,16 @@ load_world_from_json :: (json_str: string, chunk_bin: []u8) -> (World, bool) { array_add(*world.notes, note); } + for je: wj.entities { + e := spawn_entity(*world, je.type, .{je.position[0], je.position[1], je.position[2]}, je.orientation); + if e == null then continue; // unknown type; spawn_entity logged it + e.id = je.id; + if je.id >= world.next_entity_id then world.next_entity_id = je.id + 1; + for f: je.fields { + entity_apply_field(e, f.name, f.value); + } + } + for jov: wj.rdm_overrides { size := ifx jov.rdm_size > 0 then jov.rdm_size else RDM_DEFAULT_SIZE; array_add(*world.rdm_overrides, .{x=jov.x, y=jov.y, z=jov.z, rdm_enabled=jov.rdm_enabled, rdm_size=size}); diff --git a/test_game/game.jai b/test_game/game.jai index ced4652..9fec87d 100644 --- a/test_game/game.jai +++ b/test_game/game.jai @@ -1,3 +1,11 @@ +// @Entity is what registers a type with the engine; see src/entities.jai. +Test_Marker :: struct { + #as using base : Entity; + base.type = Test_Marker; + + PARTS :: Entity_Part.[]; +} @Entity + game_engine_config :: () { } diff --git a/test_game/resources/game_core/sprites/anim.sheet.json b/test_game/resources/game_core/sprites/anim.sheet.json deleted file mode 100644 index 8db367f..0000000 --- a/test_game/resources/game_core/sprites/anim.sheet.json +++ /dev/null @@ -1,702 +0,0 @@ -{ "frames": [ - { - "filename": "animsheet 0.aseprite", - "frame": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 120 - }, - { - "filename": "animsheet 1.aseprite", - "frame": { "x": 50, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 2.aseprite", - "frame": { "x": 100, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 3.aseprite", - "frame": { "x": 150, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 4.aseprite", - "frame": { "x": 200, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 5.aseprite", - "frame": { "x": 250, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 6.aseprite", - "frame": { "x": 300, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 120 - }, - { - "filename": "animsheet 7.aseprite", - "frame": { "x": 350, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 90 - }, - { - "filename": "animsheet 8.aseprite", - "frame": { "x": 400, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 9.aseprite", - "frame": { "x": 450, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 10.aseprite", - "frame": { "x": 500, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 11.aseprite", - "frame": { "x": 550, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 12.aseprite", - "frame": { "x": 600, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 13.aseprite", - "frame": { "x": 650, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 14.aseprite", - "frame": { "x": 700, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 90 - }, - { - "filename": "animsheet 15.aseprite", - "frame": { "x": 750, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 16.aseprite", - "frame": { "x": 800, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 17.aseprite", - "frame": { "x": 850, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 18.aseprite", - "frame": { "x": 900, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 19.aseprite", - "frame": { "x": 950, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 20.aseprite", - "frame": { "x": 1000, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 21.aseprite", - "frame": { "x": 1050, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 22.aseprite", - "frame": { "x": 1100, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 23.aseprite", - "frame": { "x": 1150, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 24.aseprite", - "frame": { "x": 1200, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 25.aseprite", - "frame": { "x": 1250, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 26.aseprite", - "frame": { "x": 1300, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 27.aseprite", - "frame": { "x": 1350, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 28.aseprite", - "frame": { "x": 1400, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 29.aseprite", - "frame": { "x": 1450, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 30.aseprite", - "frame": { "x": 1500, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 31.aseprite", - "frame": { "x": 1550, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 32.aseprite", - "frame": { "x": 1600, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 33.aseprite", - "frame": { "x": 1650, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 34.aseprite", - "frame": { "x": 1700, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 35.aseprite", - "frame": { "x": 1750, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 36.aseprite", - "frame": { "x": 1800, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 37.aseprite", - "frame": { "x": 1850, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 120 - }, - { - "filename": "animsheet 38.aseprite", - "frame": { "x": 1900, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 39.aseprite", - "frame": { "x": 1950, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 40.aseprite", - "frame": { "x": 2000, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 41.aseprite", - "frame": { "x": 2050, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 42.aseprite", - "frame": { "x": 2100, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 43.aseprite", - "frame": { "x": 2150, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 120 - }, - { - "filename": "animsheet 44.aseprite", - "frame": { "x": 2200, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 90 - }, - { - "filename": "animsheet 45.aseprite", - "frame": { "x": 2250, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 46.aseprite", - "frame": { "x": 2300, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 47.aseprite", - "frame": { "x": 2350, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 48.aseprite", - "frame": { "x": 2400, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 49.aseprite", - "frame": { "x": 2450, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 50.aseprite", - "frame": { "x": 2500, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 51.aseprite", - "frame": { "x": 2550, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 90 - }, - { - "filename": "animsheet 52.aseprite", - "frame": { "x": 2600, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 53.aseprite", - "frame": { "x": 2650, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 54.aseprite", - "frame": { "x": 2700, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 55.aseprite", - "frame": { "x": 2750, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 56.aseprite", - "frame": { "x": 2800, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 57.aseprite", - "frame": { "x": 2850, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 58.aseprite", - "frame": { "x": 2900, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 59.aseprite", - "frame": { "x": 2950, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 60.aseprite", - "frame": { "x": 3000, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 61.aseprite", - "frame": { "x": 3050, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 62.aseprite", - "frame": { "x": 3100, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 63.aseprite", - "frame": { "x": 3150, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 64.aseprite", - "frame": { "x": 3200, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 65.aseprite", - "frame": { "x": 3250, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 66.aseprite", - "frame": { "x": 3300, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 67.aseprite", - "frame": { "x": 3350, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 68.aseprite", - "frame": { "x": 3400, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 69.aseprite", - "frame": { "x": 3450, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 70.aseprite", - "frame": { "x": 3500, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 71.aseprite", - "frame": { "x": 3550, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 72.aseprite", - "frame": { "x": 3600, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - }, - { - "filename": "animsheet 73.aseprite", - "frame": { "x": 3650, "y": 0, "w": 50, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 50, "h": 48 }, - "sourceSize": { "w": 50, "h": 48 }, - "duration": 75 - } - ], - "meta": { - "app": "http://www.aseprite.org/", - "version": "1.3.15.5-x64", - "image": "animsheet.png", - "format": "RGBA8888", - "size": { "w": 3700, "h": 48 }, - "scale": "1", - "frameTags": [ - { "name": "player_idle", "from": 0, "to": 6, "direction": "forward", "color": "#000000ff" }, - { "name": "player_walk", "from": 7, "to": 14, "direction": "forward", "color": "#000000ff" }, - { "name": "player_run", "from": 15, "to": 22, "direction": "forward", "color": "#000000ff" }, - { "name": "player_stop", "from": 23, "to": 24, "direction": "forward", "color": "#000000ff" }, - { "name": "ball", "from": 27, "to": 27, "direction": "forward", "color": "#000000ff" }, - { "name": "jump_up", "from": 28, "to": 28, "direction": "forward", "color": "#000000ff" }, - { "name": "jump_top", "from": 29, "to": 29, "direction": "forward", "color": "#000000ff" }, - { "name": "jump_down", "from": 30, "to": 30, "direction": "forward", "color": "#000000ff" }, - { "name": "jump_land", "from": 31, "to": 32, "direction": "forward", "color": "#000000ff" }, - { "name": "slide", "from": 33, "to": 36, "direction": "forward", "color": "#000000ff" }, - { "name": "red_idle", "from": 37, "to": 43, "direction": "forward", "color": "#000000ff" }, - { "name": "red_walk", "from": 44, "to": 51, "direction": "forward", "color": "#000000ff" }, - { "name": "red_run", "from": 52, "to": 59, "direction": "forward", "color": "#000000ff" }, - { "name": "red_jump_up", "from": 65, "to": 65, "direction": "forward", "color": "#000000ff" }, - { "name": "red_jump_top", "from": 66, "to": 66, "direction": "forward", "color": "#000000ff" }, - { "name": "red_jump_down", "from": 67, "to": 67, "direction": "forward", "color": "#000000ff" }, - { "name": "red_jump_land", "from": 68, "to": 69, "direction": "forward", "color": "#000000ff" }, - { "name": "red_slide", "from": 70, "to": 73, "direction": "forward", "color": "#000000ff" } - ], - "layers": [ - { "name": "Layer 1", "opacity": 255, "blendMode": "normal" } - ], - "slices": [ - ] - } -} diff --git a/test_game/resources/game_core/sprites/anim.sheet.png b/test_game/resources/game_core/sprites/anim.sheet.png deleted file mode 100644 index 9a31876594259c6f447f7bdac0f59fada893fb9a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16446 zcmd_SRajf$wk}L@_X5FFyv0J0LUC`AVx@R-w~|09?obF$fk1F~hhhZ^#R|b)3WWx@ zVlDcoYps3u+WVYmpL73T%$a$S%sJmNzG3eeUp{DQs*n)U6JlUskUUpa)WN_Yg`%$o z@o~}TBo0qw^v45;j*2`+HIi`$1A`UgxuTq&H{u}cWt6p=*Xl-`ZgY1(D?wq8V8*EN zSw49H)=CsEnL+U+E4kS=fDl`#!6ydO1t)7QJeR&hM0mRjmjTAGY(y8_ZK#S1pg*NY z0d!*`>99jZZ;ScrR$^BCn4iAo$&{vW)Pq!%KE4xn!sb-0Q z@8@OyB`LnO&c9dOnj#eEx3{-KT1<*p;ZeR%H5qeBGgg(m=q#yOiuD`nVwbr*2e< zBv)e4dV2eX2bCMzoxQ^1i@#qdeDt1B=U08=wp`?YeHwOL7hP?j`H#Fna&^f@J~DcI zX0yeQv`k?M1Hoy&6mgxK)U~D*KVm|=ne&#eEiqrpy)f%HB9rK7d_pE68T%S@4~K~C z9O!=YHRu+LitIKNeJhvu%Aaf5HM@B|KqV@Sn_&lC6e zDyih<-8}@p)TJRXo1e(vB$6BwG-LX8VDsRpL=gPrwJs$>r1*l*F(cEi@EHU1%T@qz|3Ws~>xcH$41c@WN5<2#JJHlpo*1ZS1eOX|_Tz4hPLfAWMr zgpJ7%)>Of32K5d-4+*ckTKlbrjIO&no|d_~yOZ8_;B37VCW#06gar#4)E$6Op2Ki~;LJzjM7Y;@*SO-i*}E?W*_6iK zwd4e>wmpT^GNvUGYwqe-g#KANwxIe?*TU6fPcGEJlhj~d8(8a>V6_I(lxODg`P}Ye zk_!Lri-f0lk+6uOjZZ~Ef$N`->LXgM=b@~p-e=gSB57&$KX*5bh z7@HG@yNCW{kjo>L=bHE*0K=wxP6(+B4F5U^j9!d)@{_f0=zC09kAywbx!mKvJl64w z;nd3$uwD~>{DE9v6Ul>6Co3z9l+FHT_tMLaxSt0+_ONYc*m{jDxL6Ix2GEDLj*XA* z*E?Ij61-L&W?nAe-2Feb2ft|IiP<)HD7Zzh4E*=7PsYX+Xo~?;5vGl0hb_I7i3GVj z_7A&;C*i6YD%+8dC3m!Ngf(tjxV`&5R^(H#bRF_?fPtc3()2O+H&!#~CqPZul4_(4 zZ0=jN3!s_|&(tqRFS9<1G*xu*FPMaZsP?9Zz&;jnaGtyPa(ph`QxPkh7WwC)MZ5*z zziJ$GreP4&kpsVVAG`YVH9zn)?Cfojx^w#c=gvJ`sqM3+mUo7y(ZEv`mem~Pz#tK( z$*efU`e2IUi^89VG19gf!t%-{dyR*lmO5-e-E_++*rnF0zZb3O-0OAJ%$_U6-}8$O z4S=sb(RV37t}e772&%p=d+I)JDYh+VBNqSk6Bm1!b%^bcwe0g=TB6&wcr0tn-agSV zSAUfJXyJF7ktY!&EdEh$)DLH&s@Fo!eW0oW+YZ9!eqIZ={d#?YXw#K#ACMl)P_s>? zG^sFMj!Lu-%$_{y;G5Px`2~t%ebW5@h;ZW;f?ThVBLUei>4iBkEk(Av&dIJ&J;p<%~bcQ+8t+WOgikyq0np2vo%TSNneRu$gU%Fl<9yUK5iay@>7Q(z>22vppUE05K4Ad(KQ+VL5zs!JGH&G?1qPC`I> zQO*eKLS03$y6}1mgwI#^`91#Wf4N}lg`^_)?DH0aq8c}MHzQ@P>aSaKZU?q_7=f{L zU`VQZn1IathU<5|QYpJ52p+!rt?~H;dPj}2p7}fsHiOPF4Q6Fq3r(&t>LJ`rI`Agr zwxIKwgrFLoNG*)sxoUU3l7udt2>`6d*#iyoMT+f(JOzgj=(=*_cV7JVfBO8nNfGz#<%)U;; zEZrjPPyLYaKgw7}*bsk(1ku#CGp@ri(M+$Z{MgihB#gcLl*hXlOm@pg zHWn47k3mf?q7Rf5#}`}PV_&Z zlG%bLb=F&}lk8mBC^G<6guadN4BkDrZ;8}#*(Qb)7d)^MsCdZ-({V~3FyAKr|atx!*hZnJlD;j}D))UIZoq?vmamd#(3sXa}kee2u1J=rIIra5u9 zi<5xFYRvF=%%K{4|}) z1CKEXEYU*ZPw;+KH3PrM?R&u0`%kxLkmmfXG=%4R zy4C9Xg5M9V(~B!<#p=|(wIpWKfE|_(Ub7WMm@6{PZbxg^%Hhb7c!@hJ!7I?;SCYc8SLn0Kf+uonk7r&(Pc>DtL2hoaUK~`j3 z<@MU}Fz!-C0e^iblD2K%{j^QFe=|KTr(@xOubF$!igWcz>-%#Fztgn959hK`eC46G zW|HZwy|(Pe7HrWH{4+S!Y(q9b=!ac36-GvTX?N`hZ3>?}{3{y;tsHv?(`?LSOj^0R z>b&wB4k`wWSM(kmw7`dqBLuBjq}_n5rK?VX^g{Kp$riUrB4KYIvOksQ$|)bcL~K#S zADSLw{(`^Tg|M*Ap7dM?OuI$S8GTPnIWytBQpwNiIf&f7s}mU)({Zu9zGHS!=^&Nc zVSpS4HqzWe9GM+X5#SRs0kF$`y7_%xOYs#v7{XKMKK9x?jW_N3f3hJ-=pRswD<^)_{RHqk>;JZQ#%~I55 z^wpQd6|9laFl((U5XA9EuYsoNXUZmJ>d5(rnA_xa-JrGCHer@|L>!gmN^d zvbM3Km)V#gs$4Eot^#Yaew$X5gjF{e#6@vm`|IvcbcZg3NK%K@$%r$|2GZ zPf@Ac`W55fzrQRNEPhd*ve;@MkEK9M|fxIL|A*`az3?s=33kgsBkn zGMPsZE0btWGeu*?K80IL}AtR%PVs(XC?gi?`!UIC6^Yk2`z z2db$;Y@a3)eNIFvqG0MsayXa?2+(sHL1g?EOw~8hIv<;o{<05DBUXeedS7JO7c6Mx|2s$t*4_-Q z$G&A!P1R}iR3#>@^}~B&A4X#GdL+DG6l<@0D5~sX8NIOGidHYEN_UPJNp~`St99pz zZvi&3D!fvQX@e*C{3;9PP2~V@VTaN-D$A$ZM#FVsVLr>QQv7>LZw`VvL^y9C;AZcc zwllw}tJ9jzw~||azkir<0jtE}c~snjXWSjz_;HOecp*k?Lc@u9Sfdt~h<~pUMrz*O zAy14Y{)N!8%_t&5SY00PumI43&=D4PzydxUZ8$)eq{M3(Nq_i7i!dGe>30$bR&{V| z=AVKh7>N^vULxMEysC_`>MzlETFcn4@ret(Tv*}vrPg`#9-Y-R0VosCPh)ERec!n| z=4%ZTieLDbSi0h>ZGxUqLp>`V&IN@@x(nKEk0gi~FxTMl1a|PFb1{7UOIXvTM&{lV zRjYDk(&L?@qj!c{vCCApAgw#;w;w-`Nti)%Pq`N#YMF0qdrwd>Ism}WAJ7BsHhSe8_X}*&0+kE<#Ax`3u9!9#X0`?X!vbS$*oSMtDTQbJn19XcN5s3f4 zLMy3P0iJ(+M>UcVRmrj_dna!9qiK3^4*}U+CQco+(&ygZ{e;V|_%t01wH>7Ajh_oi zkugiFWce)2BH(zGDVp1_7t6RdRl!@Td`Qqh$9-Sm&0KVxkHu8#lG@wxPj{I4c&b{RnV zFJ}$8i`_twi0I1Na0$oHJtFBrNhvV#B+v!Ic8EjBz({@s!Q4dQ!K*up_=3)dnbzpv zPfD`(Poq`3GP4G>OZ&$E20q$KMS@Reue;s!0wJ~h2zR*in{rK!;{CYcUQA@Da3QKE zgO=%mg_JStQg!~=vb;Ymgn9mT3?36MrdoOFOfWSJu~A0{tXj*<@qHxPDEU=*h8g>F z9N;a~R!vq4NGvAbL_cV_tqa&E{p@fQjKHm3E%nGW5J{rSk(r3I*K`Moc`99D7EH~)f|uWLmE zw9YBB)m-NcBXy6JJP_YT__ z5YevX54c5h3MQ)bS5iQp*B7L&NX?!RL>LTAM_2(9t7o1!c@uuH)J|$&Rk;;n)Ydl> zQr?vXm$@400@78)dz&?SytF)FE^O3Hf*sij=)sM!)~{J=t9lD5>d)Yzvnmu=!Njk; zCxG4r`ixWSqxc`l^-RC`w9{9_(3=0hfd%~=tdSxBmufHxjfqqC{UXEBpHn&v~!bTwN;n z44#8lWoq7(C947>G1@^A>b4bR=k>s7X z&-wJ#E5b&8&fkXi+agVEzX6mrfBW)iIQyi$uN5M)($!J6$SN}%i?N3kXqV3^p}E^0 zpRXYw1VssaCbX~3*p^$K#D0~Hpk{`uk2rpqwln!3kr6cDC2(3sr4UfgVDO9#>EVvc zT);F2;jXBJt7Q^EM2tbkQlQUjyoKZz;t#~h=4xEo4MSm*y|1H1TS69=^{jsc@v`8T zk)UV4BJOhPgQGuN2sn%E=nK@)*{4F9C&jPiQ6Bqua#r9i{)V53aEhzW6i!j8VMzmc zcpVQQA%yNWTFi!92$ zU>qro!ojLgeL_C4%o9PQr@Mt%GY#`d_74j2;>rx>&yiZ#Qxh69u(zWjY367I5w2Sp zh5Su1efTJwIIu55i~>(8WQ@;SUw9$WhOz01ywY9}58ur-b3vs<%3DS)o4nB&cE^|B zl2aL;UMS}c6&Jm3{dslAh((4ego>u%K{4VrdJDPpkD{)kb4Y+V-7sa9^VdZ1V;yD9 zy*_%yxt|SOf7uueD47Sf?BydMvCH%C3We(LWc}OQ-uO&d%-}mznli=Gbz+2^(e{Lp zs>vOQr$474!qrdIXN_ysbuHG?$by8tqZrn>ctE(A<&4_~qt6`y(=5W3LR|=4U?KiU zaf|;2r|uWz<33_(#B+8FVUd=lTS~e+V~bolm9vRPB(0N&Aw%O*${Jo7;zFF!0S)C@ zDCbBhghEZ$Wo-n}yu?KoEgUxh3-Qw@wm)qldTFUaJpTm3KjV3NOzH7a4+|0x3LQ{ZKKtQ5n9jG;=yHsz+lD+$=Un`=?x&KitxWOyZ zakmo4-~Q3Vw@*tZ$J!Cis$!YOE-n=ABXHHTvXGqIn~U0yd&$zX$tmRyPVU2JahtGx zPjiayct?hiJoHq6j@_SMFFBKAEiKZic&Fx$^mjSCa33tl44G|E-+ zptHPG)yqfWy`T{|z7>{*dwy>|#iVXKVVt@dno;jn$W1C~w&|3XoKb@15j!owl<4hj z4@hUG4HZnhp8ULs$_tPlnx5u>ia6RO*F)$9J-s9{a}%6Yc(}O!Ply&iQOABDHmk}Z zJG@&%Bl{#{9b5L?sHK7vAkk?KWV6=D zt-xW5Cz&OHzbLO^AURgUG#45a0*^~O>^gtdBN;qjPteRfvLsH&-x}u_G2>L7!85p5 zmG-W)=R{x`n1+?1o&kvHkxVOfxD7CZNk$y2avr_nnM3qq|KSIzel*j_=d!CFYdWb} zz*~P@DtpUG_RTXDd511NE7{p#0Ol&XN1fY(gA=E|U+kyZ?aLG$su&m~Cq~{e!#$3A zt_)H~?7l~mU(E}0`!me=rH%hkQo-+sC1fwoSoO&(4`#@|CX!%gsH775sP{&Uek~2V z+d9F@`o1ethw7H<3bbTGx2Y<8gDTKYP2OM7CA*Pr3M_oe+wJ{z`G zzh0P^h*T9JaH}Bm+WigAwJcv{7?1KZC?7T!Xv16RZ9lrW`I~p4nFTTwLHB2n=g2NP z)SPt0<=eo}WbTY{X}k_+eCF$#UM)Oso9wS>&thq^&>5k}0&X~ke#l+1H7d4GxEgmM zE0gvx$X0>2HjxqpD2;7{Idc8`+~37ri0rvxFho3iXudpi$P@*7I594c9dI-1MT%Tvwhpu5`n?b|s zDTFeMz&2;jA4;xRHx;1}mct5jmch?LU+p8S!_8T38DDZD(>3tX8QL-Qw~YDsbO~F} z%-&l)YCbDyCbVjqlW<8=2%+@j=7YC;7dCXywz)wgcbN zDY$0YE4?IeGR(FzE5F69C!VxBZ*zA(jwM9|b;rbMiOSnHK#O77d~#)Ew%hF07^BVv z53S##g|Yv8QQ(Gnvcp|i6MXKFTW`d#n4COB^F_%Y!*)MYqs7Rk;UnOy$GsrD^(Bg8 zXfmZuYxl>mh2;D{dUnhv=+em}FXauJ(c{s^w_WzDDt-8g^u9kp&qpZCIhUU2u=sc( zGtPaSQLyi>E5(kgXS7s5mAR0UhV8}ZOz^<}&@zC%#749EMIUjR8Ps~4idI+zkNW4K zxa?t-M5{owHvG%GxvM|ne|#6w3>N{KlAda#OHPMG5zk*uU=Cpo5HqiBC=EPV#OHY( z7#Qg8KnX|$9zW-Jw1I)avj2A$K#L`e^+Co0<>SF0_#NBxCn9#&tp9-&yN~%?-DDci zy#Mn8G~C%nxo%lT-S#l~z4fLXy`rf}T>8tem7;XhUwl5d{@ygv-_~A}+G6}%+@I~O zP6@$IAM#p6w-KH=veZyeW{Oj}^~Sev>Ixf7>MNE8qq-HN$Ms_rPL6Hof!DeqT#}Vtqq9H3|V2fPH@0}d$7H;nY5`b-0wvu00N9tBS?I>jKkBA5Ri z_(3dy*^iXbc*83K=2=EObF%CYUY>e%Wcu?^s`10A(hb(nUItdxnU|!surl3?S}#`n zbf-3jrPspc8@Z#b>8y6O>2XDS8_wEH+db}022i$=Gw^Tb-Ga96O5|wE(PsbC4 zJdL;re+3&D*bIZ7)Ybsi{8G8k6Z4JHjkp<=>Nl*|`uzCO5y)~2wYo6eibaiehtE_V zhBf2YGxJvptI2NxprDpQIkOf~H}8G65DJ%tro6+wq>p9BRafXFv-+dOC*!KNySo7B zy=d|+>Eg5@(ber5*f;O3k|&75bzQR_{nltK%O!L5%kLc*s0oL z6n+$NbiIQ2=Kir!^MO^Po~*#ER6Q5aEU4^ zoIklE8|ZKe#=NHgaW7s{9&gemi(hF%PEC^asyEe=fJDD3^UIVU*Jiz0-epDId1Ob4 zVs~sO2g$FtruV&8EEF*NZ`CB8Z@ToHRu2+Hs&P#5P2X75f3xli74@7A36S}u==A~{ zT=-YF_aZkNV>2=L*1sQfj^&66$K#vgueSrCENeDPt5$Rh*Dm=o19(DFXwv^WKQmH5 z>s&W8d@2ME4qyE?9NW8%=V-5}P|$nGd!Y95INwTpoVc0W&t~UMx)2!(Q#AsEc#ses zgq6_#6bvxzi%B&YKwS-|4CybvB2@G5GnaXZyYqh3KoKbqI)>mEZx~TUEq1Tsvf^-# zd<~6AFV8hN?q1k``qSpH-S2|^{rTmk4`T#lOv&tnXrN-6mAZnnx{}i3DBFYjmnn;? z%gD_DK_NG>JhYZwn|3`8xGWMDKhEU`>*{_g5vhtn#NNHW?>&m_y8RV%-0~)%?y~>; z?e7?=&@J=*F#47kkot6Nncr8hds_mJ;xph#Cv*oGpU2AY;I{cmcMI-T0lYv8m_KP zbCS<7TL-eOKOj6eFN>$!-x^|-ssY3d8gk4h$7~AjBso5YjsFX`W;Rnv$B2I# zyy81cB(TE4ktvr>9xC21+}E$C$A3V_oQ8o0O-S7WZe@XuENN^r_b>3z1yJIaU5<&z zj%9}X8cnqgE^Obc;IG$2s-12n_Mlg?e5`SixS@IoOBng*m2s$9?^S0RB!2P4Q*cf0 z{0}hM-kaVOIE*B4MJ+#h?fU&iSMjA11!{oa<9a&v!h7h4H>g;C58Y;1Ko2M(@n2C1 zM|)4UvN1h-u^?_W-h^(flBb6BCpM~T&8Kt=c6&8V9hcm1;dMp^WN)COXYMXh=Zz`a zlF(6Y2)FgUAf?av+K18eS6dvV9{8U{mzvE2e)V_Y%34narpY<*{?-YJf7MS0+3t3y zE-)wM`OZk?Xua>Od?q>Zwp1IM8(=4ES1{YylD-~2f4I4tUs+^*R13}fXu6>kNei1(DJfWS3Q?wbDec{9dTPK-AmjrI^$21Ql)p;634%N_Y4K`(Qcx8RJY|q zxkQ0bvyjJ;YfcR=dqr3zK^=j}GBa*q&%oo2NO7q7(`aqM$P~9Yf+jq;u1C(>D$c_0 z8_yIMXLT$enW0fP<=OC8_E?)X_xkO>pc>5#BmpFn!-eP+f5;7#f*$^1zm0MoIi?GxV`WnwEPHTmZ&awEL7W}J z)AdC`CI{1!i9L~*sudQBkNQX-1m>Lm!J}s9b<_F&yzRTm8#x^2mp}hzsBlC3DE4SG z2E^=`z|~*Zs$`XT$fi%6*jjh-Yk`Uzt#Fr&+6==+>zgP{&lp5S6$vU?I;_@Ckq4kV zFnph0Kh2uBcaI(lY0J*kab&P^`uaCTM_rY-Mz}V;sE%jWM$Jjt>u|80-T(N0KX9Df zb@lBZr%o|T-4YRWIlALZPB#49W`_~W=0<~J4^5@7n0aXuU6a4P7}rZhPBMlz_R~%o z{X}>pVcgGqJqdZ*E5lpdj+C>XXFU{0XhvIlo*3Zje&@l6=35K;^){2w z*xEq7R>YP;>M{x{7}Jk%;B=BWx&h&l0n#|&N%n7w{2Qzx?~8EPU>};ihbQ4ofo}5K zV&=gEy6?+`8pg?=Q+J9!ty0+TdS=Pnrg6g{ZXiM3zo;hoZ$e4;IQ6M+7EOQ2oDI>x zxm}9>r649Wuz7Xw)rFSFS4L6 zXPK~Abnw&6)E4F?n`;YI%DvlVOpPsjTdSikhv#f6HOp$ISINL?^`cml$Xl=|4jtRx z{@H)-upGkVRARX@?a+DiKsdqYU;Sf!xcK>4F1A`4H(Vm|IRKv{s3molb3BOva(iP* z+ff1T7&skn9c@8(yzfvmPA;?CUAMl0DI&$u>oQ_2`F1<7L{TgDQ!B5L$w&ak#K7wz z#TwF-r?Q#(u<04ntRkwcySuyG%Bq1^mKk@yzTdSUe`LA&dWZw^if+}l+q-FJ39S1z z{UNmotbC=kvwFN@_pW^;)sAa+zinwk`<<QKTa0?=s1PgDdF^-?*AqiL*|xc)h5^7MzcE% z2`?7~d=|Zo|0)kRF8XK2nX+n}BTa;>Gl12No{MGF(kOXUCC!1*2s;yA1}#$C6jpmVlblCNYtPPZJO*jp=g8yBXbJcM;Lkjp zkutC05)<{%e$_7S6~RtjINz9F<>Oh;Vx((<5O+RPWpUXwmejjK>3Kw%?(FH?RLw$P zW&g3@aeEYFlE;F%hI9li6Ba7A46%eX`EfoQp8WVE zb;lW*9V7PdP4z^yq0efRgE(4WoH7cr5cxDg5>N5Z_^Xs&$gR_$ z)MGaETx`E7a;B2gr^lh&6|MatkRsEv^TX^`FlyOF1D5su>ii9WL@2J|`Pba@WV9P- z?@6d1XdgSw1;-N-eq4kSrs~Og2nW`yYJmG&*FpfSt z=b-(X)6Q9~y`0lPk}pUEb-Zy|Yl+B5ydJptf=;G{t}iJ!WI#`+7@1pdI#8^cgrYk| zjO^iG?xUG1P%*NKOqi)D*Y3D1{2$2W-uT*gY!6y!1f|`r!$L7~5ImF7z{xhASMKKO z8jozr3(dzd4HZ-g%{7vtLAukJT6lQDt3^$!JlxA)5Dsq<<)hB=8cdKOX9Kx)ouc=g`H;fK-Ki}RK{@V+P>tLUNLey z8*6wO^f9FjTpI4@yvyzo2O1mdh#U9r*gm#Y&oXA&nypLE1D~7ES$biONl@ErDqna+ zH_$RM3uCL?O7`dH*}z86KZ^I(a@y@;3;NECyM14MKP0-Fgm%oh%|=AfSa20!o8^_^ zhS6SB!(|~%tgejvpd9vc@S!=6EtP_B4=`39GGTLgjy-V76Sj2}5DLnG{JNW4G z{PG=Wy?8{c)XLqvv?BDgE00=9F*;97VU|Zxq7}QXo>etr<%SUP8=?M?o;3v)LHnVt zxPQc?pT2OW1|B{5=Gr%7M-(E#DeR7(1Kcci zwST+|I}d|B+EQ*|P{X~N+~NTMlgGph{#^tv#U@vRR8>Vb-5##~6`rbZb|{^c3(cL@ zmDZ(*A^|CfS)z~IZH9FiS%n^ zl{JX??8i1l#KRv^G(dccId!>yEmKhLm1%ZnFXZf<-F!b2x2uqm{6hINY5$au%R@jS zgvx5S?)WA*Kr#pvFZc%?s968Q61F=BwKKUhju)NWDg8Wr0Z%XAZem^lE z=)|rnblKRJBU4pqs#H#QH=A^0L76P%wOKWue%}SRWaTaE^{l$|&LU@3l z{`D%Vr_au+lRcW(OSv}d0^9P%rRxH=17bSw0NqI=&1Us4#`M~ zb8TAej96^O4#T);O$q6@S&k0H_;EMw$gIbi%p^p!;~zfVCivPG5NgZ-@i3H7=%02V zIg{3Kp12*2SGW|D3HaSIJX_WPHOjN?1lnpIi}**HW1A%X?Z$51p{Fi*P{}O0n{scQz%&sBmd9!tf^Yhpq)|9TTvoY}}{aGn- zn*UWC65dFF|C@^U?pcCTE9Gj7ugc9^g}(C&+cK*0tJw{vOH>7~mVDp~5HbM{&;GGl zx6>*!baN(z=LK(*!-CZ6^>ePjZ3i&Dz2%?I==CVpjM_rvM)vJ^Ff~L@Eu!sWfYR5o zN+c|Ncf&*i-KrhQ`r$fizow+3N~arYajL?|ZZ+dq0^P@i^3A9)rt{VqPm&YLy`F`J z0c-n9pC9CsDf+k084ity3PSqzhIWe@5+5+iCq8nZR|V)&gwT{wW{yuR=?=3aGGwsV zKu-W2T>aDY7*mt@hOC*UY$XLE;*cy+NSwdGBZ<;8bk$J^rxLLbZ41@pKTMBCMA+4N3nPw?iq=0SC>>vNb>)jT{XqD~|>ooCxv+HVe9v@;c| zlM$dMqHA2XwS9xf$q{fwpWL5l)ALVMl@j~ZOA#JB#$ zhzzR`HUIA-y32-?kI6K#OkW8GQS*SUt9A6k<>nZ^SR|;~kvzm&{nF)yuARRmE{Kcp ziSQ1ILlL|RT#mEpW!~d?SZX@K2C*S#b`&^6pM>NIQ+E|)1*sWB*8^5#dQ|ZazdpF3 z!CbS{d$URuBrm+hP1;{r+Cc-;c@C)(eT1%`-7iME(fbTO6O}d!+2Oy9sKH1{tPCmS z5uwj)J3l3|W*aPp>)yo_D_oi~QD+e%X7`lXNpz!T4cu>mA06Dv0XhZ}YwwANoZ;>0 zDa>r6JKIxBWRQ+7*_9Lr?|bh9kii|q{}f_zxlAczm|UPpv4Hc zM`|;|o@>A7qbo{68M!D%#g?t@!-L&k$rgPSSBL`p;pD=sAF^wdP0UYM)y1+F*lPX} z$RJ&L=cxrP&)YtKloY|E=*0W!E@8M)Y=)SJm+4rt-*&SNZuUP*sd)r~9N80#C3`j> zG5(#oT4ilfi*M=6R_SP2n0oGY>~?rgY2dx*S@zw%3{$|J9!{p(f%FFoaEY<##G=wg z1bG8=_r0|AC!jJ*iu z|FTwRBBE*0V#y<4vn%7r4s|Ji4X-3kkafxsA5(TI?&}Pw5lRGN3vqxUkC{qy{NoC< zbJd^Yir8E~sLeK&(M;DNyNb-d52}YA#wz*viHP>=yEGdcUPezS*ONx{n8Y9)gl#rc zWRE~#>q-Z7u~V7z2GJQvyqCWz;(1#Rrs+2%eyMjOWxf25j(2X9rejCaUH)hbNld!p zBGe>cXG+9Y^MUa3d*z(Y}Qi_tdIh2&>fk8H`%&O-H%`TfHrtafU zaRJ8o)vX!~#p>oOBRtlamls!^!-^3n=xNsc$>6JnE!ACuVLxjv-DVbyy@m6QFrM>c z@scYttyE-ZooakdfB$^j7Gmqkn0C|zs>r7bYfLyp$m27Sr|qD@lo!6Yw6=D`@@A}K zasy-s7XFr|_bXAhZ2#zJA93_{VgHjI0fx2ns>Yp9HXs`h5)mRy5}A)aJK-Dy=-YLO7I~P&csvS_exS9{TgIQ}74-!IeR_om61{P~(73r#HMKnM>n27t) z+iQ9HR{_KgG2H0dJ2DbJSFZtlKufe~9(=KvR5=Y$XLGJT9>n7z|~5 zu=t<`mGQbE^s*8UckNY%yzRc?1DxsclMTd*;w&^Gk;V9~LDU!O@3gh-kvCDwRdC&> z<4rC#0%bk!39geP0GmOl z5vTc-KXpS8B-O1MExLh-UMQQGHRm^xpFZ=!#Z|7(kOqx)T>%6@A%>ROGrAE3hZKW( z)c_qJQ5~I!rI72Eh`L9YO}Tq%R)_W&aLuN)o^uhNckajRXBEiy_UBDuM>7;Vz87bFtML(bev+VHd51g}_A_d-@zBfe+EAxk_$S&<4& z(qrKnO7xoV?%$yX)}P4yj#+UEJa!AU-(~q>twxjI9h`whEtSr^jpekNGPy(6XeAc@HmSZwt2V>Lq6~)zgh0_b7 zR0o)k&=cb^x6-&$y0dy;Wx@#ekV*vH8T8lHTJ>6-eP zQbak`H8%4twC0A2I27+eJ<0j#3^}G3YC3f7`DQ({+!m0*u&?LSxYqZHo_*W_n@!o{ zN*>Ca^?n^&O-y-56LMX9Zv&(FB^ zIQM=eSb%0|I=7$7URz##9I9Q4JgN~Ai^m361MnE| z2T#2K>ZvJgM;LF`nG79y@tj`(^0wiA!uF3I!nQ9zmN6O|ZYL?q`- zOe3!DCd#DPV;PUub0EqB1|$40SCi>!`=%FipC0DIOQi-f#NC3fb2mefN8ET^#UtbTgWL1lh;WkyuRQfK9txCBJGROu&X&>LNeR`Q}14G z#@utox@ozPxSJ7T{7-eoU5DXGI^qpQON{~$|I4Q(pR}D<5 zV+HJX_vqeZ%uM_w4(hf+))9&FrZAnDX_fK>QlQ^Sn;s)J$uODP+^~Wtg{di!(YiMC z96h>Xs`k(k_ZbsJ#DF19_ZII}=!|Fbj7PE%24p0-tUG^V+7jLg<^q@CfUQYrpnlZy zuZ6_pI^#pomYyZ*wMzs&i1Ul0ghwsQ4E4xhTH~m4%vk6vjbf^0q h`z1(d_TS?vtZ{jK&M@af{~-Xzb0tm1YI*aJ{|^L7e6|1p diff --git a/test_game/resources/game_core/sprites/score.sheet.json b/test_game/resources/game_core/sprites/score.sheet.json deleted file mode 100644 index f15d006..0000000 --- a/test_game/resources/game_core/sprites/score.sheet.json +++ /dev/null @@ -1,267 +0,0 @@ -{ "frames": [ - { - "filename": "scoresheet 0.aseprite", - "frame": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 1.aseprite", - "frame": { "x": 96, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 2.aseprite", - "frame": { "x": 192, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 3.aseprite", - "frame": { "x": 288, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 4.aseprite", - "frame": { "x": 384, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 5.aseprite", - "frame": { "x": 480, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 6.aseprite", - "frame": { "x": 576, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 7.aseprite", - "frame": { "x": 672, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 8.aseprite", - "frame": { "x": 768, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 9.aseprite", - "frame": { "x": 864, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 10.aseprite", - "frame": { "x": 960, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 11.aseprite", - "frame": { "x": 1056, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 12.aseprite", - "frame": { "x": 1152, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 13.aseprite", - "frame": { "x": 1248, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 14.aseprite", - "frame": { "x": 1344, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 15.aseprite", - "frame": { "x": 1440, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 16.aseprite", - "frame": { "x": 1536, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 17.aseprite", - "frame": { "x": 1632, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 18.aseprite", - "frame": { "x": 1728, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 19.aseprite", - "frame": { "x": 1824, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 20.aseprite", - "frame": { "x": 1920, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 21.aseprite", - "frame": { "x": 2016, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 22.aseprite", - "frame": { "x": 2112, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 23.aseprite", - "frame": { "x": 2208, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - }, - { - "filename": "scoresheet 24.aseprite", - "frame": { "x": 2304, "y": 0, "w": 96, "h": 48 }, - "rotated": false, - "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 96, "h": 48 }, - "sourceSize": { "w": 96, "h": 48 }, - "duration": 100 - } - ], - "meta": { - "app": "http://www.aseprite.org/", - "version": "1.3.16.1-x64", - "format": "RGBA8888", - "size": { "w": 2400, "h": 48 }, - "scale": "1", - "frameTags": [ - { "name": "lscore_0", "from": 0, "to": 0, "direction": "forward", "color": "#000000ff" }, - { "name": "lscore_1", "from": 1, "to": 1, "direction": "forward", "color": "#000000ff" }, - { "name": "lscore_2", "from": 2, "to": 2, "direction": "forward", "color": "#000000ff" }, - { "name": "lscore_3", "from": 3, "to": 3, "direction": "forward", "color": "#000000ff" }, - { "name": "lscore_4", "from": 4, "to": 4, "direction": "forward", "color": "#000000ff" }, - { "name": "lscore_5", "from": 5, "to": 5, "direction": "forward", "color": "#000000ff" }, - { "name": "lscore_6", "from": 6, "to": 6, "direction": "forward", "color": "#000000ff" }, - { "name": "lscore_7", "from": 7, "to": 7, "direction": "forward", "color": "#000000ff" }, - { "name": "lscore_8", "from": 8, "to": 8, "direction": "forward", "color": "#000000ff" }, - { "name": "lscore_adv", "from": 9, "to": 9, "direction": "forward", "color": "#000000ff" }, - { "name": "rscore_0", "from": 10, "to": 10, "direction": "forward", "color": "#000000ff" }, - { "name": "rscore_1", "from": 11, "to": 11, "direction": "forward", "color": "#000000ff" }, - { "name": "rscore_2", "from": 12, "to": 12, "direction": "forward", "color": "#000000ff" }, - { "name": "rscore_3", "from": 13, "to": 13, "direction": "forward", "color": "#000000ff" }, - { "name": "rscore_4", "from": 14, "to": 14, "direction": "forward", "color": "#000000ff" }, - { "name": "rscore_5", "from": 15, "to": 15, "direction": "forward", "color": "#000000ff" }, - { "name": "rscore_6", "from": 16, "to": 16, "direction": "forward", "color": "#000000ff" }, - { "name": "rscore_7", "from": 17, "to": 17, "direction": "forward", "color": "#000000ff" }, - { "name": "rscore_8", "from": 18, "to": 18, "direction": "forward", "color": "#000000ff" }, - { "name": "rscore_adv", "from": 19, "to": 19, "direction": "forward", "color": "#000000ff" }, - { "name": "score_middle", "from": 20, "to": 20, "direction": "forward", "color": "#000000ff" }, - { "name": "score_board", "from": 21, "to": 21, "direction": "forward", "color": "#000000ff" }, - { "name": "score_board_l", "from": 22, "to": 22, "direction": "forward", "color": "#000000ff" }, - { "name": "score_board_r", "from": 23, "to": 23, "direction": "forward", "color": "#000000ff" }, - { "name": "logo", "from": 24, "to": 24, "direction": "forward", "color": "#000000ff" } - ], - "layers": [ - { "name": "Layer 1", "opacity": 255, "blendMode": "normal" } - ], - "slices": [ - ] - } -} diff --git a/test_game/resources/game_core/sprites/score.sheet.png b/test_game/resources/game_core/sprites/score.sheet.png deleted file mode 100644 index f18cd77efdc68ea961ae6921f9c0a84cdf76ad26..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6514 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