commit 9d65ca32c4d3f4094ec643403bb737c8f84a1f2c Author: harper Date: Thu May 14 20:14:48 2026 -0700 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..879f1a4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +export/ +captures/ diff --git a/.luarc.json b/.luarc.json new file mode 100644 index 0000000..039a3c3 --- /dev/null +++ b/.luarc.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json", + "runtime.version": "Lua 5.4", + "runtime.nonstandardSymbol": ["+=", "-=", "*=", "/=", "%="], + "workspace.library": ["./meta"], + "format.defaultConfig": { + "indent_style": "space", + "indent_size": "2" + }, + "diagnostics": { + "globals": ["vim", "love"], + "disable": ["lowercase-global"] + } +} diff --git a/engine/class.lua b/engine/class.lua new file mode 100644 index 0000000..31a60f1 --- /dev/null +++ b/engine/class.lua @@ -0,0 +1,7 @@ +Class = {} +Class.__index = Class + +function Class:new(tbl) + tbl = tbl or {} + setmetatable(tbl, { __index = self }) +end diff --git a/engine/collision.lua b/engine/collision.lua new file mode 100644 index 0000000..4e7b663 --- /dev/null +++ b/engine/collision.lua @@ -0,0 +1,61 @@ +collision = {} +collision.colliders = {} + +collision.debug = false + +function collision.draw() + for _, v in pairs(collision.colliders) do + gfx.rect(v.x, v.y, v.w, v.h, gfx.COLOR_BLUE) + end +end + +function collision.add(tbl) + table.insert(collision.colliders, tbl) +end + +function collision.remove(index) + table.remove(collision.colliders, index) +end + +function collision.move(aabb, dx, dy) + local normal = { x = 0, y = 0 } + if aabb.normal ~= nil then + aabb.normal = normal + end + + -- move x + aabb.x = aabb.x + dx + + for _, v in pairs(collision.colliders) do + if aabb ~= v and collision.aabb_check(aabb, v) then + if dx > 0 then + aabb.x = v.x - aabb.w + normal.x = -1 + elseif dx < 0 then + aabb.x = v.x + v.w + normal.x = 1 + end + end + end + + -- move y + aabb.y = aabb.y + dy + + for _, v in pairs(collision.colliders) do + if aabb ~= v and collision.aabb_check(aabb, v) then + if dy > 0 then + aabb.y = v.y - aabb.h + normal.y = -1 + elseif dy < 0 then + aabb.y = v.y + v.h + normal.y = 1 + end + end + end + + return normal +end + +function collision.aabb_check(a, b) + return a.x < b.x + b.w and b.x < a.x + a.w and a.y < b.y + b.h and b.y < a.y + a.h +end diff --git a/engine/vector.lua b/engine/vector.lua new file mode 100644 index 0000000..844ecaf --- /dev/null +++ b/engine/vector.lua @@ -0,0 +1,24 @@ +vector = {} +vector.utils = {} + +function vec(x, y) + return { + x = x, + y = y, + } +end + +function vector.utils.input_vector() + local vec = { x = 0, y = 0 } + vec.x = vector.utils.bool_to_int(input.held(input.RIGHT)) - vector.utils.bool_to_int(input.held(input.LEFT)) + vec.y = vector.utils.bool_to_int(input.held(input.DOWN)) - vector.utils.bool_to_int(input.held(input.UP)) + return vec +end + +function vector.utils.bool_to_int(b) + if b == true then + return 1 + else + return 0 + end +end diff --git a/main.lua b/main.lua new file mode 100644 index 0000000..9272b67 --- /dev/null +++ b/main.lua @@ -0,0 +1,48 @@ +require("player") +require("engine.vector") +require("engine.class") +require("engine.collision") +require("world") + +function _config() + return { + name = "Game", + pixel_perfect = true, + game_id = "com.harper.usagitest", + } +end + +collision.add(Player.aabb) + +for i = 1, 10 do + local new_block = { + color = gfx.COLOR_DARK_GRAY, + aabb = { + x = (i - 1) * 10, + y = 50, + w = world.cell_size, + h = world.cell_size, + }, + } + + world.utils.set_tile(new_block.aabb.x, new_block.aabb.y, new_block) +end + +world.utils.update_colliders() + +function _init() + State = {} +end + +function _update(dt) + Player:update(dt) +end + +function _draw() + gfx.clear(gfx.COLOR_BLACK) + Player:draw() + for _, v in pairs(world.tiles) do + gfx.rect_fill(v.aabb.x, v.aabb.y, 10, 10, v.color) + end + collision.draw() +end diff --git a/meta/usagi.lua b/meta/usagi.lua new file mode 100644 index 0000000..ffce8c3 --- /dev/null +++ b/meta/usagi.lua @@ -0,0 +1,608 @@ +---@meta +-- Generated by usagi 0.6.1. Run `usagi refresh` to update. +-- Usagi API stubs for lua-language-server. +-- Declarations only; this file is never executed by the runtime. + +---Pico-8 palette, indices 0-15. +---@class Usagi.Gfx +---@field COLOR_BLACK integer 0 +---@field COLOR_DARK_BLUE integer 1 +---@field COLOR_DARK_PURPLE integer 2 +---@field COLOR_DARK_GREEN integer 3 +---@field COLOR_BROWN integer 4 +---@field COLOR_DARK_GRAY integer 5 +---@field COLOR_LIGHT_GRAY integer 6 +---@field COLOR_WHITE integer 7 +---@field COLOR_RED integer 8 +---@field COLOR_ORANGE integer 9 +---@field COLOR_YELLOW integer 10 +---@field COLOR_GREEN integer 11 +---@field COLOR_BLUE integer 12 +---@field COLOR_INDIGO integer 13 +---@field COLOR_PINK integer 14 +---@field COLOR_PEACH integer 15 +gfx = {} + +---Clears the screen to the given color. +---@param color integer a gfx.COLOR_* constant +function gfx.clear(color) end + +---Draws text at (x, y) in the given color. Uses the bundled monogram +---font at its 16px design size (a 5×7 pixel font with 16px line height). +---@param text string string to render +---@param x number left edge in game-space pixels +---@param y number top edge in game-space pixels +---@param color integer a gfx.COLOR_* constant +function gfx.text(text, x, y, color) end + + +---Draws a rectangle outline. +---@param x number left edge in game-space pixels +---@param y number top edge in game-space pixels +---@param w number width in pixels +---@param h number height in pixels +---@param color integer a gfx.COLOR_* constant +function gfx.rect(x, y, w, h, color) end + +---Draws a filled rectangle. +---@param x number left edge in game-space pixels +---@param y number top edge in game-space pixels +---@param w number width in pixels +---@param h number height in pixels +---@param color integer a gfx.COLOR_* constant +function gfx.rect_fill(x, y, w, h, color) end + +---Draws a circle outline centered at (x, y). +---@param x number center x in game-space pixels +---@param y number center y in game-space pixels +---@param r number radius in pixels +---@param color integer a gfx.COLOR_* constant +function gfx.circ(x, y, r, color) end + +---Draws a filled circle centered at (x, y). +---@param x number center x in game-space pixels +---@param y number center y in game-space pixels +---@param r number radius in pixels +---@param color integer a gfx.COLOR_* constant +function gfx.circ_fill(x, y, r, color) end + +---Draws a line from (x1, y1) to (x2, y2). +---@param x1 number start x in game-space pixels +---@param y1 number start y in game-space pixels +---@param x2 number end x in game-space pixels +---@param y2 number end y in game-space pixels +---@param color integer a gfx.COLOR_* constant +function gfx.line(x1, y1, x2, y2, color) end + +---Sets a single pixel. +---@param x number x in game-space pixels +---@param y number y in game-space pixels +---@param color integer a gfx.COLOR_* constant +function gfx.pixel(x, y, color) end + +---Draws a 16×16 sprite from the loaded sheet at (x, y). The sheet is +---`sprites.png` next to the game's main .lua; indices run left-to-right, +---top-to-bottom. Alpha-channel pixels render as transparent. +---@param index integer one-based sprite index (1 = top-left cell) +---@param x number destination left edge in game-space pixels +---@param y number destination top edge in game-space pixels +function gfx.spr(index, x, y) end + +---Extended `spr`: draws a 16×16 sprite with required flip flags. Same +---indexing as `gfx.spr`. +---@param index integer one-based sprite index (1 = top-left cell) +---@param x number destination left edge in game-space pixels +---@param y number destination top edge in game-space pixels +---@param flip_x boolean flip horizontally (mirror left/right) when true +---@param flip_y boolean flip vertically (mirror top/bottom) when true +function gfx.spr_ex(index, x, y, flip_x, flip_y) end + +---Draws an arbitrary (sx, sy, sw, sh) rectangle from `sprites.png` at +---(dx, dy) at its original size. `s*` args index into the source sheet +---in pixels; `d*` args are the destination on screen. +---@param sx number source rect left edge on `sprites.png` (pixels) +---@param sy number source rect top edge on `sprites.png` (pixels) +---@param sw number source rect width in pixels +---@param sh number source rect height in pixels +---@param dx number destination left edge in game-space pixels +---@param dy number destination top edge in game-space pixels +function gfx.sspr(sx, sy, sw, sh, dx, dy) end + +---Extended `sspr`: source rect stretched to (dw, dh) at the destination +---with required flip flags. All ten args required; write a thin +---wrapper if a particular flag combination shows up often in your +---code. +---@param sx number source rect left edge on `sprites.png` (pixels) +---@param sy number source rect top edge on `sprites.png` (pixels) +---@param sw number source rect width in pixels +---@param sh number source rect height in pixels +---@param dx number destination left edge in game-space pixels +---@param dy number destination top edge in game-space pixels +---@param dw number destination width in pixels (stretches the source) +---@param dh number destination height in pixels (stretches the source) +---@param flip_x boolean flip horizontally (mirror left/right) when true +---@param flip_y boolean flip vertically (mirror top/bottom) when true +function gfx.sspr_ex(sx, sy, sw, sh, dx, dy, dw, dh, flip_x, flip_y) end + +---Activates a post-process fragment shader. Loads `shaders/.fs` +---(and optional `.vs`) and runs it as the final pass when the +---game render target is blitted to the window. Pass nil to clear. +---On web the loader prefers `_es.fs` (GLSL ES 100); on desktop +---it prefers `.fs` (GLSL 330). Shader source live-reloads on +---save in `usagi dev`. +---@param name string|nil shader name (file stem under `shaders/`), or nil to clear +function gfx.shader_set(name) end + +---Sets a uniform on the active shader. The value type drives the +---uniform type: a number maps to float, a 2/3/4-length numeric table +---maps to vec2 / vec3 / vec4. Queues the write; the engine flushes +---queued uniforms once per frame before the post-process pass. +---@param name string uniform name as declared in the shader source +---@param value number|number[] float, or {x, y} / {x, y, z} / {x, y, z, w} +function gfx.shader_uniform(name, value) end + +---@class Usagi.Sfx +sfx = {} + +---Plays a sound effect by name. Names are file stems from the `sfx/` +---directory next to the game's main .lua (e.g. `sfx/jump.wav` → "jump"). +---Unknown names silently no-op. Calling while already playing restarts. +---@param name string file stem of a `.wav` under `sfx/` +function sfx.play(name) end + +---@class Usagi.Music +music = {} + +---Plays a music track once and stops at the end. Names are file stems +---from the `music/` directory next to the game's main .lua (e.g. +---`music/intro.ogg` → "intro"). Recognized extensions: ogg, mp3, wav, +---flac. Stops the currently-playing track first if there is one. +---Unknown names silently no-op. Callable from `_init` so a title +---track can start the moment the window opens. +---@param name string file stem under `music/` +function music.play(name) end + +---Plays a music track and loops it forever. Stops the currently- +---playing track first. Callable from `_init`. +---@param name string file stem under `music/` +function music.loop(name) end + +---Stops whatever music is currently playing. No-op when nothing is. +function music.stop() end + +---Abstract input actions. Each is a union over keyboard keys, gamepad +---buttons, and analog-stick directions: +--- +---- LEFT: arrow left, A, dpad left, left stick left +---- RIGHT: arrow right, D, dpad right, left stick right +---- UP: arrow up, W, dpad up, left stick up +---- DOWN: arrow down, S, dpad down, left stick down +---- BTN1: Z, J; gamepad south face (Xbox A, PS Cross) +---- BTN2: X, K; gamepad east face (Xbox B, PS Circle) +---- BTN3: C, L; gamepad north + west face (Xbox Y/X, PS Triangle/Square) +--- +---Mouse buttons (separate from the action constants above): +--- +---- MOUSE_LEFT: left mouse button +---- MOUSE_RIGHT: right mouse button +--- +---Source identifiers for `input.last_source()` and the source-aware +---`input.mapping_for`: +--- +---- SOURCE_KEYBOARD: "keyboard" +---- SOURCE_GAMEPAD: "gamepad" +---@class Usagi.Input +---@field LEFT integer +---@field RIGHT integer +---@field UP integer +---@field DOWN integer +---@field BTN1 integer +---@field BTN2 integer +---@field BTN3 integer +---@field MOUSE_LEFT integer +---@field MOUSE_RIGHT integer +---@field SOURCE_KEYBOARD string +---@field SOURCE_GAMEPAD string +---@field KEY_A integer +---@field KEY_B integer +---@field KEY_C integer +---@field KEY_D integer +---@field KEY_E integer +---@field KEY_F integer +---@field KEY_G integer +---@field KEY_H integer +---@field KEY_I integer +---@field KEY_J integer +---@field KEY_K integer +---@field KEY_L integer +---@field KEY_M integer +---@field KEY_N integer +---@field KEY_O integer +---@field KEY_P integer +---@field KEY_Q integer +---@field KEY_R integer +---@field KEY_S integer +---@field KEY_T integer +---@field KEY_U integer +---@field KEY_V integer +---@field KEY_W integer +---@field KEY_X integer +---@field KEY_Y integer +---@field KEY_Z integer +---@field KEY_0 integer +---@field KEY_1 integer +---@field KEY_2 integer +---@field KEY_3 integer +---@field KEY_4 integer +---@field KEY_5 integer +---@field KEY_6 integer +---@field KEY_7 integer +---@field KEY_8 integer +---@field KEY_9 integer +---@field KEY_F1 integer +---@field KEY_F2 integer +---@field KEY_F3 integer +---@field KEY_F4 integer +---@field KEY_F5 integer +---@field KEY_F6 integer +---@field KEY_F7 integer +---@field KEY_F8 integer +---@field KEY_F9 integer +---@field KEY_F10 integer +---@field KEY_F11 integer +---@field KEY_F12 integer +---@field KEY_SPACE integer +---@field KEY_ENTER integer +---@field KEY_ESCAPE integer +---@field KEY_TAB integer +---@field KEY_BACKSPACE integer +---@field KEY_DELETE integer +---@field KEY_LEFT integer +---@field KEY_RIGHT integer +---@field KEY_UP integer +---@field KEY_DOWN integer +---@field KEY_LSHIFT integer +---@field KEY_RSHIFT integer +---@field KEY_LCTRL integer +---@field KEY_RCTRL integer +---@field KEY_LALT integer +---@field KEY_RALT integer +---@field KEY_BACKTICK integer +---@field KEY_MINUS integer +---@field KEY_EQUAL integer +---@field KEY_LBRACKET integer +---@field KEY_RBRACKET integer +---@field KEY_BACKSLASH integer +---@field KEY_SEMICOLON integer +---@field KEY_APOSTROPHE integer +---@field KEY_COMMA integer +---@field KEY_PERIOD integer +---@field KEY_SLASH integer +input = {} + +---Returns true the frame any source bound to `action` first went down. +---@param action integer one of input.LEFT / RIGHT / UP / DOWN / BTN1 / BTN2 / BTN3 +---@return boolean +function input.pressed(action) end + +---Returns true while any source bound to `action` is held. +---@param action integer one of input.LEFT / RIGHT / UP / DOWN / BTN1 / BTN2 / BTN3 +---@return boolean +function input.held(action) end + +---Returns true the frame any source bound to `action` first went up +---(transitioned from held to released). Mirrors `input.pressed` for the +---release edge. +---@param action integer one of input.LEFT / RIGHT / UP / DOWN / BTN1 / BTN2 / BTN3 +---@return boolean +function input.released(action) end + +---Label of the active input source's primary binding for `action` (e.g. +---"Z" on keyboard, "Pad-A" on gamepad). Honors any keymap remap the +---player set via the pause menu's Configure Keys flow. Useful for +---rendering contextual control prompts. Returns `nil` for unknown +---actions or when the active source has no binding for `action`. +---@param action integer one of input.LEFT / RIGHT / UP / DOWN / BTN1 / BTN2 / BTN3 +---@return string? +function input.mapping_for(action) end + +---The input source that most recently fired any bound action. Returns +---`input.SOURCE_KEYBOARD` ("keyboard") or `input.SOURCE_GAMEPAD` +---("gamepad"). Switches only when a *bound* input fires, so menu keys +---and idle activity don't flip it. +---@return string matches one of input.SOURCE_KEYBOARD / input.SOURCE_GAMEPAD +function input.last_source() end + +---Cursor position in game-space pixels (so it lines up with `gfx.*` +---coords regardless of window size or pixel-perfect scaling). Returns +---two values: `x, y`. When the cursor sits over the letterbox bars, +---the values fall outside `0..usagi.GAME_W` / `0..usagi.GAME_H` — +---bounds-check before treating them as in-game coords. +---@return integer x game-space x in pixels +---@return integer y game-space y in pixels +function input.mouse() end + +---Returns true while the given mouse button is held. +---@param button integer one of input.MOUSE_LEFT / input.MOUSE_RIGHT +---@return boolean +function input.mouse_held(button) end + +---Returns true the frame the given mouse button first went down. +---@param button integer one of input.MOUSE_LEFT / input.MOUSE_RIGHT +---@return boolean +function input.mouse_pressed(button) end + +---Returns true the frame the given mouse button first went up +---(transitioned from held to released). +---@param button integer one of input.MOUSE_LEFT / input.MOUSE_RIGHT +---@return boolean +function input.mouse_released(button) end + +---Returns true while the given keyboard key is held. +--- +---Direct keyboard reads bypass the keymap override and gamepad +---bindings — prefer `input.held(action)` for game actions players +---should be able to remap or play with a controller. Use this for dev +---hotkeys (toggling debug overlays, F-key shortcuts) and for +---keyboard-and-mouse-only games. +---@param key integer one of the input.KEY_* constants +---@return boolean +function input.key_held(key) end + +---Returns true the frame the given keyboard key first went down. See +---`input.key_held` for the bypass-the-keymap caveat. +---@param key integer one of the input.KEY_* constants +---@return boolean +function input.key_pressed(key) end + +---Returns true the frame the given keyboard key first went up +---(transitioned from held to released). See `input.key_held` for the +---bypass-the-keymap caveat. +---@param key integer one of the input.KEY_* constants +---@return boolean +function input.key_released(key) end + +---Show or hide the OS cursor over the game window. Persists until +---changed. Callable from `_init` so games can hide the cursor before +---the first frame draws (e.g. when rendering a custom in-game cursor). +---@param visible boolean true to show, false to hide +function input.set_mouse_visible(visible) end + +---Returns true when the OS cursor is currently shown over the window. +---Reflects the latest `input.set_mouse_visible` call synchronously, so +---it's safe to use as part of a toggle: +---`input.set_mouse_visible(not input.mouse_visible())`. +---@return boolean +function input.mouse_visible() end + +---Engine-level info. The per-domain APIs (`gfx`, `input`) are top-level +---globals, not fields on this table. +---@class Usagi +---@field GAME_W number game render width in pixels +---@field GAME_H number game render height in pixels +---@field SPRITE_SIZE integer side length, in pixels, of one cell in `sprites.png` (drives `gfx.spr` indexing) +---@field IS_DEV boolean true under `usagi dev`; false for `usagi run` and compiled binaries +---@field elapsed number wall-clock seconds since session start; updated once per frame before _update +usagi = {} + +---Measures `text` in the bundled font and returns its rendered size +---in pixels. Returns two values: `width, height`. Available from any +---callback (`_init`, `_update`, `_draw`) — useful for pre-computing +---layout once in `_init` and reusing the result every frame. +---@param text string string to measure +---@return integer width pixel width +---@return integer height pixel height (equals the font's line height) +function usagi.measure_text(text) end + +---Persist a Lua table as JSON. Saves are per-game, namespaced by +---`game_id` from `_config()`. One file per game; nest your own +---structure inside (settings, run state, unlocks). +---@param t table table to serialize. functions, userdata, NaN, and cycles error +function usagi.save(t) end + +---Read the persisted save table back. Returns `nil` on first run +---(no save file). Idiomatic call: `state = usagi.load() or { ... defaults ... }`. +---@return table? +function usagi.load() end + +---Config table returned by `_config()`. All fields optional except +---`game_id`, which is only required if you call `usagi.save` / +---`usagi.load`. Missing fields fall back to engine defaults. +---@class Usagi.Config +---@field name? string display name. Window title, macOS .app bundle directory, and (slugged) archive/binary names on `usagi export` (default: project directory name) +---@field pixel_perfect? boolean false (default) = any scale that fits the window while preserving aspect ratio; true = integer scale only with letterbox bars +---@field game_id? string reverse-DNS identifier (e.g. "com.you.mygame"), required for save/load +---@field icon? integer 1-based tile index into sprites.png to use as the window icon (same indexing as gfx.spr); omit for the default Usagi bunny +---@field game_width? number game render width in pixels (default 320). Tested range 160..640 +---@field game_height? number game render height in pixels (default 180). Tested range 90..360 +---@field sprite_size? integer side length, in pixels, of one cell in sprites.png (default 16). Drives gfx.spr indexing, the tilepicker tool's grid, and the window-icon slicer. sprites.png must be a multiple of this value on both axes. + +---Optional. Returns engine config read once before the window opens. +---Omit if the defaults are fine. +---@return Usagi.Config? +function _config() end + +---Called once when the game starts. Use for loading assets and initializing state. +function _init() end + +---Called every frame to update game state. Runs before _draw. +---@param dt number delta-time: seconds since last frame +function _update(dt) end + +---Called every frame to render. Runs after _update. +---@param dt number delta-time: seconds since last frame +function _draw(dt) end + +---@class Usagi.Vec2 +---@field x number +---@field y number + +---@class Usagi.Rect +---@field x number +---@field y number +---@field w number +---@field h number + +---@class Usagi.Circ +---@field x number +---@field y number +---@field r number + +---Drop-in math/geometry helpers. Pure Lua, no engine state. Source +---lives in `runtime/util.lua` — read it for full implementations or +---fork it if you want different semantics. +---@class Usagi.Util +util = {} + +---Clamps `v` into `[lo, hi]`. +---@param v number +---@param lo number +---@param hi number +---@return number +function util.clamp(v, lo, hi) end + +---Returns -1, 0, or 1 according to the sign of `v`. +---@param v number +---@return integer +function util.sign(v) end + +---Half-up rounding to the nearest integer. Pixel snapping is the +---driving use case in 2D pixel-art games. +---@param v number +---@return integer +function util.round(v) end + +---Moves `current` toward `target` by at most `max_delta`, never +---overshooting. Per-frame smoothing primitive — pass a delta +---scaled by `dt` for frame-rate independence. +---@param current number +---@param target number +---@param max_delta number +---@return number +function util.approach(current, target, max_delta) end + +---Linear interpolation. `t = 0` returns `a`, `t = 1` returns `b`. +---Values of `t` outside `[0, 1]` extrapolate (no clamping). +---@param a number +---@param b number +---@param t number +---@return number +function util.lerp(a, b, t) end + +---Wraps `v` into `[lo, hi)`. Useful for cyclic values like angles or +---looped indexing. Works for negative `v`: `util.wrap(-1, 0, 4) == 3`. +---@param v number +---@param lo number +---@param hi number +---@return number +function util.wrap(v, lo, hi) end + +---Boolean from time. Toggles `hz` times per second — the on/off +---interval is `1/hz` seconds. For invincibility flicker, UI blinks, +---low-health warnings. +---@param t number seconds +---@param hz number toggles per second +---@return boolean +function util.flash(t, hz) end + +---Normalizes a `{x, y}` vector to unit length. Returns a new table; +---the input is unchanged. A zero vector returns `{x = 0, y = 0}`. +---@param v Usagi.Vec2 +---@return Usagi.Vec2 +function util.vec_normalize(v) end + +---Distance between two `{x, y}` points. +---@param a Usagi.Vec2 +---@param b Usagi.Vec2 +---@return number +function util.vec_dist(a, b) end + +---Squared distance between two `{x, y}` points. Cheaper than +---`vec_dist` (skips the sqrt); use for "is X closer than Y?" by +---comparing against `r * r`. +---@param a Usagi.Vec2 +---@param b Usagi.Vec2 +---@return number +function util.vec_dist_sq(a, b) end + +---Builds a vector at `angle` (radians) with magnitude `len`. `len` +---defaults to 1 for a unit vector. Pair with `math.atan(dy, dx)` to +---convert any direction into a velocity. +---@param angle number radians +---@param len? number magnitude (default 1) +---@return Usagi.Vec2 +function util.vec_from_angle(angle, len) end + +---True when the `{x, y}` point is inside the rect `{x, y, w, h}`. +---Half-open: left/top edges are inside, right/bottom edges are +---outside. Matches typical sprite-rect hit testing. +---@param p Usagi.Vec2 +---@param r Usagi.Rect +---@return boolean +function util.point_in_rect(p, r) end + +---True when the `{x, y}` point is strictly inside the circle +---`{x, y, r}`. Points on the boundary are considered outside. +---@param p Usagi.Vec2 +---@param c Usagi.Circ +---@return boolean +function util.point_in_circ(p, c) end + +---True when the two AABBs share interior area. Edge-adjacent rects +---are considered non-overlapping. +---@param a Usagi.Rect +---@param b Usagi.Rect +---@return boolean +function util.rect_overlap(a, b) end + +---True when the two circles overlap. Tangent circles are +---considered non-overlapping. +---@param a Usagi.Circ +---@param b Usagi.Circ +---@return boolean +function util.circ_overlap(a, b) end + +---True when a circle and a rect overlap. Uses the closest-point +---method: clamp the circle center to the rect, test distance. +---@param c Usagi.Circ +---@param r Usagi.Rect +---@return boolean +function util.circ_rect_overlap(c, r) end + +---Engine-level juice primitives: hitstop, screen shake, flash, and +---slow-motion. Each call sets per-session state that decays once per +---frame. Stacking rule across all four: longer duration wins; for +---the magnitude param, the latest call wins. Spam-calling is safe. +effect = {} + +---Freezes the game's `_update` loop for `time` seconds. `_draw` keeps +---running so the world stays on-screen. The classic juice trick for +---weighty hits: pair with `effect.screen_shake` and `effect.flash` on +---impact. If a longer hitstop is already in flight, this call is a +---no-op (longer wins). +---@param time number seconds to freeze update +function effect.hitstop(time) end + +---Shakes the rendered view for `time` seconds with up to `intensity` +---game-pixel offset. Magnitude decays linearly to zero across the +---duration. The shake is applied to the RT-to-screen blit, so +---overlays drawn outside the world (error, REC indicator) stay +---stable. +---@param time number seconds to shake +---@param intensity number maximum offset in game pixels (try 2-6) +function effect.screen_shake(time, intensity) end + +---Flashes a full-screen overlay of palette color `color` over the +---rendered view for `time` seconds. Alpha decays linearly from +---opaque to transparent. White on hits, red on damage, etc. +---@param time number seconds the flash is visible +---@param color integer a gfx.COLOR_* constant +function effect.flash(time, color) end + +---Scales the `dt` passed to `_update` for `time` seconds. `scale=0.5` +---is half-speed; `scale=0` freezes update (use `effect.hitstop` for +---that explicitly); `scale>1` plays faster. Wall-clock decay is +---unaffected; the slow_mo timer itself counts down at real time. +---@param time number seconds the scale is applied +---@param scale number dt multiplier; 0..1 for slow, >1 for fast +function effect.slow_mo(time, scale) end diff --git a/player.lua b/player.lua new file mode 100644 index 0000000..1640784 --- /dev/null +++ b/player.lua @@ -0,0 +1,58 @@ +require("engine.vector") + +Player = { + speed = 1, + velocity = vec(0, 0), + aabb = { + x = 0, + y = 0, + w = 10, + h = 10, + normal = {}, + }, + pointer = vec(0, 0), + looking_at = nil, +} + +function Player:update(dt) + self:input() + if self.aabb.normal.y ~= -1 then + self.velocity.y += 10 * dt + end + + local targeting = world.utils.get_tile(self.pointer.x, self.pointer.y) + if targeting ~= nil then + self.looking_at = targeting + else + self.looking_at = nil + end + + collision.move(self.aabb, self.velocity.x, self.velocity.y) +end + +function Player:draw() + gfx.rect_fill(Player.aabb.x, Player.aabb.y, 10, 10, gfx.COLOR_WHITE) + gfx.circ(self.pointer.x + (world.cell_size / 2), self.pointer.y + (world.cell_size / 2), 3, gfx.COLOR_WHITE) +end + +function Player:input() + local input_dir = vector.utils.input_vector() + input_dir = util.vec_normalize(vector.utils.input_vector()) + self.velocity.x = input_dir.x * self.speed + self.pointer = world.utils.to_tile( + self.aabb.x + (input_dir.x * world.cell_size), + self.aabb.y + (input_dir.y * world.cell_size) + ) + + if input.pressed(input.BTN1) and self.aabb.normal.y == -1 then + self.velocity.y = -3 + end + + if input.pressed(input.BTN2) and self.looking_at ~= nil then + self.looking_at.aabb = nil + for i, v in pairs(collision.colliders) do + print(v.aabb.x, v.aabb.y, v.aabb.w, v.aabb.h) + end + world.utils.set_tile(self.pointer.x, self.pointer.y, nil) + end +end diff --git a/usagi b/usagi new file mode 100755 index 0000000..8059aca Binary files /dev/null and b/usagi differ diff --git a/usagi.bak b/usagi.bak new file mode 100755 index 0000000..391c6fc Binary files /dev/null and b/usagi.bak differ diff --git a/world.lua b/world.lua new file mode 100644 index 0000000..79fc391 --- /dev/null +++ b/world.lua @@ -0,0 +1,24 @@ +world = {} +world.tiles = {} +world.utils = {} +world.cell_size = 10 + +function world.utils.update_colliders() + for i, v in pairs(world.tiles) do + collision.add(v.aabb) + end +end + +function world.utils.to_tile(x, y) + return vec(util.round(x / world.cell_size) * world.cell_size, util.round(y / world.cell_size) * world.cell_size) +end + +function world.utils.set_tile(x, y, tbl) + local key = x .. "," .. y + world.tiles[key] = tbl +end + +function world.utils.get_tile(x, y) + local key = x .. "," .. y + return world.tiles[key] +end