Compare commits

...

21 Commits

Author SHA1 Message Date
b25e41bd49 not working FUCK YOU MATH 2025-03-03 02:05:43 +03:00
91c2e1bce1 new aabb constructor 2025-03-03 00:44:54 +03:00
1433aa8e40 aabb drawing for debug 2025-03-03 00:30:34 +03:00
e3f195cf71 dang 2025-03-02 23:44:58 +03:00
342c98e915 add rotation field to obj 2025-02-23 17:08:20 +03:00
1e2a162800 add in-place rotate method to v3 and avoid unnecessary conversion in :rotated() 2025-02-23 17:07:56 +03:00
3859c190d1 vector3 typing improvements 2025-02-23 16:13:36 +03:00
db4809d385 add obj loader 2025-02-21 02:03:50 +03:00
82da17287e fix vector3 fields in server 2025-02-21 02:03:08 +03:00
71c045c797 add music 2025-02-20 23:40:42 +03:00
b7e027cdc6 some type stuff 2025-02-16 00:19:54 +03:00
4c5006d1e7 add predicate and reduce function doc 2025-02-15 18:04:00 +03:00
6a952a221b ducks choose the closest feed 2025-02-15 18:03:43 +03:00
954277688f don't rely on shallow copy to construct new classes. also multiple ducks 2025-02-15 17:25:27 +03:00
d4c79731b0 duckie :) 2025-02-15 14:58:27 +03:00
40c9af4803 better Vector3 coercion semantics 2025-02-15 13:37:39 +03:00
4900c50850 add credits and duckie 2025-02-14 22:54:57 +03:00
651ea584af add font 2025-02-14 22:43:33 +03:00
ea4504bdb4 add acceleration to player 2025-02-14 22:03:18 +03:00
a0ba6f9da3 add lerp function to util and lerp method to vector3 2025-02-14 22:02:19 +03:00
a532325cd2 move util into global scope 2025-02-14 22:01:50 +03:00
16 changed files with 909 additions and 65 deletions

5
CREDITS.txt Normal file
View File

@ -0,0 +1,5 @@
PATH: NAME - HYPERLINK - LICENSE
data/images/duckie.png: ChickenTeddy - https://opengameart.org/content/duckie - CC-BY 4.0
data/images/measure001a.png: KenneyNL - https://www.kenney.nl/assets/prototype-textures - CC0
data/images/tongue.png: KenneyNL - ? - CC0

Binary file not shown.

BIN
data/images/duckie.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
data/images/measure002a.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

38
data/models/unit_cube.obj Normal file
View File

@ -0,0 +1,38 @@
# Blender 4.0.2
# www.blender.org
o Cube
v -0.500000 -0.500000 0.500000
v -0.500000 0.500000 0.500000
v -0.500000 -0.500000 -0.500000
v -0.500000 0.500000 -0.500000
v 0.500000 -0.500000 0.500000
v 0.500000 0.500000 0.500000
v 0.500000 -0.500000 -0.500000
v 0.500000 0.500000 -0.500000
vn -1.0000 -0.0000 -0.0000
vn -0.0000 -0.0000 -1.0000
vn 1.0000 -0.0000 -0.0000
vn -0.0000 -0.0000 1.0000
vn -0.0000 -1.0000 -0.0000
vn -0.0000 1.0000 -0.0000
vt 0.375000 0.000000
vt 0.625000 0.000000
vt 0.625000 0.250000
vt 0.375000 0.250000
vt 0.625000 0.500000
vt 0.375000 0.500000
vt 0.625000 0.750000
vt 0.375000 0.750000
vt 0.625000 1.000000
vt 0.375000 1.000000
vt 0.125000 0.500000
vt 0.125000 0.750000
vt 0.875000 0.500000
vt 0.875000 0.750000
s 0
f 1/1/1 2/2/1 4/3/1 3/4/1
f 3/4/2 4/3/2 8/5/2 7/6/2
f 7/6/3 8/5/3 6/7/3 5/8/3
f 5/8/4 6/7/4 2/9/4 1/10/4
f 3/11/5 7/6/5 5/8/5 1/12/5
f 8/5/6 4/13/6 2/14/6 6/7/6

BIN
data/music/bg1.xm Normal file

Binary file not shown.

View File

@ -0,0 +1,108 @@
local Vector3 = require "types.vector3"
local Signal = require "types.signal"
local TEXTURE = "images/duckie.png"
local States = {
IDLE = 1,
WANDER = 2,
CHASE = 3,
}
local WANDER_TIME = 1.0
local IDLE_TIME = 1.0
local Duck = {}
Duck.__index = Duck
function Duck.new(position)
-- local d = util.shallow_copy(Duck)
local d = {
position = position:copy(),
velocity = Vector3(),
direction = Vector3(0, 0, -1):rotated(Vector3.UP, util.random_float(-math.pi, math.pi)),
target = nil,
state = States.IDLE,
wander_timer = 0,
speed = 0.02,
chase_speed = 0.04,
accel = 0.5,
STATES = States,
AteFeed = Signal.new(),
SeekFeed = Signal.new(),
index = 1,
}
d.position.y = 0.4
return setmetatable(d, Duck)
end
function Duck:tick(ctx)
if self.state == States.IDLE then
self:idle(ctx.frame_duration)
elseif self.state == States.WANDER then
self:wander(ctx.frame_duration)
elseif self.state == States.CHASE then
self:chase(ctx.frame_duration)
end
draw_billboard{position = self.position, size = {x = 0.4, y = 0.4}, texture = TEXTURE}
end
function Duck:idle(delta)
self.wander_timer = self.wander_timer + delta
if self.wander_timer >= IDLE_TIME then
self.state = States.WANDER
self.wander_timer = 0
self.direction = Vector3(0, 0, -1):rotated(Vector3.UP, math.random() * (math.pi * 2.0))
return
end
end
function Duck:wander(delta)
self.wander_timer = self.wander_timer + delta
if self.wander_timer >= WANDER_TIME then
self.state = States.IDLE
self.wander_timer = 0
self.SeekFeed:emit(self)
return
end
local target_vel = self.direction * self.speed
self.velocity = self.velocity:lerp(target_vel, self.accel)
self.position = self.position + self.velocity
end
function Duck:chase(delta)
local hpos = self.position:horizontal()
local fhpos = self.target.position:horizontal()
local dist = hpos:distance_to(fhpos)
if dist <= 0.1 then
self.AteFeed:emit(self.target)
self.wander_timer = 0
self.state = States.IDLE
self.target = nil
else
local dir = self.position:horizontal():direction_to(self.target.position:horizontal())
local target_vel = dir * self.chase_speed
self.velocity = self.velocity:lerp(target_vel, self.accel)
self.position = self.position + self.velocity
end
end
function Duck:start_chase(feed)
if self.state == States.CHASE then return end
-- print("duck " .. self.index .. " starting chase")
self.state = States.CHASE
self.target = feed
feed.occupied = true
end
return Duck

View File

@ -1,23 +1,21 @@
local Vector3 = require "types.vector3"
local util = require "util"
local TEXTURE = "images/tongue.png"
local Feed = {
position = {},
direction = {},
velocity = {},
landed = false,
speed = 0.15,
}
local Feed = {}
Feed.__index = Feed
function Feed.new(p_position, p_direction)
local f = util.shallow_copy(Feed)
f.position = p_position:copy()
f.direction = p_direction:copy()
function Feed.new(position, direction)
local f = {
position = position:copy(),
direction = direction:copy(),
velocity = Vector3(),
landed = false,
occupied = false,
speed = 0.15,
}
f.velocity = f.direction * f.speed
f.velocity.y = 0.1

View File

@ -0,0 +1,133 @@
---@class Obj
---@field model string
---@field texture string
---@field texture_size integer
---@field position Vector3
---@field rotation Vector3
---@field triangles table
local Obj = {}
local Vector3 = require "types.vector3"
Obj.__index = Obj
local function string_split(str, delimiter)
local res = {}
local i = 0
local f
local match = '(.-)' .. delimiter .. '()'
if string.find(str, delimiter) == nil then
return {str}
end
for sub, j in string.gmatch(str, match) do
i = i + 1
res[i] = sub
f = j
end
if i ~= 0 then
res[i+1] = string.sub(str, f)
end
return res
end
---parses the obj file and triangulates it for drawing
---@param model string path to obj model
---@param texture {file: string, size: number}
---@param position Vector3?
---@return Obj
function Obj.create(model, texture, position)
local pos
if position ~= nil then
pos = position
else
pos = Vector3()
end
local obj = {
model = model,
texture = texture.file,
texture_size = texture.size,
position = pos:copy(),
rotation = Vector3(),
triangles = {}
}
obj.position = obj.position:copy()
setmetatable(obj, Obj)
-- adapted from https://github.com/karai17/lua-obj/blob/master/obj_loader.lua
-- Copyright (c) 2014 Landon Manning - LManning17@gmail.com - LandonManning.com
-- MIT
local file = file_read{file = obj.model}
local lines = string_split(file, "\r?\n")
local obj_data = {
v = {},
vt = {},
f = {},
}
for _, line in ipairs(lines) do
local l = string_split(line, "%s+")
if l[1] == "v" then
local v = {
x = tonumber(l[2]),
y = tonumber(l[3]),
z = tonumber(l[4]),
}
table.insert(obj_data.v, v)
elseif l[1] == "vt" then
local vt = {
x = tonumber(l[2]) * obj.texture_size,
y = tonumber(l[3]) * obj.texture_size,
}
table.insert(obj_data.vt, vt)
elseif l[1] == "f" then
local f = {}
for i = 2, #l do
local split = string_split(l[i], "/")
local v = {}
v.v = tonumber(split[1])
if split[2] ~= "" then v.vt = tonumber(split[2]) end
-- v.vn = tonumber(split[3])
table.insert(f, v)
end
table.insert(obj_data.f, f)
end
end
for _, face in ipairs(obj_data.f) do
for i = 2, #face - 1 do
local triangle = {
v0 = obj_data.v[face[1].v],
v1 = obj_data.v[face[i].v],
v2 = obj_data.v[face[i + 1].v],
uv0 = obj_data.vt[face[1].vt],
uv1 = obj_data.vt[face[i].vt],
uv2 = obj_data.vt[face[i + 1].vt],
texture = obj.texture
}
table.insert(obj.triangles, triangle)
end
end
return obj
end
local function rotate_vertex(obj, v)
local vec = Vector3(v)
--- YXZ (yaw pitch roll) minimizes gimbal lock
-- return vec:rotate(Vector3.UP, obj.rotation.y):rotate(Vector3.LEFT, obj.rotation.x):rotate(Vector3.FORWARD, obj.rotation.z)
return vec:rotate(Vector3.UP, obj.rotation.y):rotate(Vector3.LEFT, obj.rotation.x):rotate(Vector3.FORWARD, obj.rotation.z)
end
function Obj:draw()
for _, triangle in ipairs(self.triangles) do
local newt = util.shallow_copy(triangle)
newt.v0 = rotate_vertex(self, triangle.v0) + self.position
newt.v1 = rotate_vertex(self, triangle.v1) + self.position
newt.v2 = rotate_vertex(self, triangle.v2) + self.position
draw_triangle(newt)
end
end
return Obj

View File

@ -1,6 +1,6 @@
local Vector3 = require("types.vector3")
local util = require("util")
local Signal = require("types.signal")
local AABB = require("types.aabb")
local Player = {
position = Vector3(0, 1, 0),
@ -12,16 +12,90 @@ local Player = {
yaw = 0,
yaw_speed = 0.05,
accel = 0.3,
aabb = nil,
test_intersect = nil,
ThrowPressed = Signal.new(),
}
local aabb_offset = Vector3(-0.25, -1, -0.25)
---@param aabb AABB
---@param other AABB
---@param velocity Vector3
---@return Vector3, Vector3
local function resolve_collision(aabb, other, velocity)
local my_min = aabb.min
local my_max = aabb:get_max()
local other_min = other.min
local other_max = other:get_max()
local overlap_x = math.min(my_max.x - other_min.x, other_max.x - my_min.x)
local overlap_y = math.min(my_max.y - other_min.y, other_max.y - my_min.y)
local overlap_z = math.min(my_max.z - other_min.z, other_max.z - my_min.z)
local min_overlap = math.min(overlap_x, overlap_y, overlap_z)
local new_pos = my_min:copy()
local new_vel = velocity:copy()
if min_overlap == overlap_x then
if velocity.x > 0 then
new_pos.x = other_min.x - (my_max.x - my_min.x)
elseif velocity.x < 0 then
new_pos.x = other_max.x
else
if my_min.x < other_min.x then
new_pos.x = other_min.x - (my_max.x - my_min.x)
else
new_pos.x = other_max.x
end
end
new_vel.x = 0
elseif min_overlap == overlap_y then
if velocity.y > 0 then
new_pos.y = other_min.y - (my_max.y - my_min.y)
elseif velocity.y < 0 then
new_pos.y = other_max.y
else
if my_min.y < other_min.y then
new_pos.y = other_min.y - (my_max.y - my_min.y)
else
new_pos.y = other_max.y
end
end
new_vel.y = 0
elseif min_overlap == overlap_z then
if velocity.z > 0 then
new_pos.z = other_min.z - (my_max.z - my_min.z)
elseif velocity.z < 0 then
new_pos.z = other_max.z
else
if my_min.z < other_min.z then
new_pos.z = other_min.z - (my_max.z - my_min.z)
else
new_pos.z = other_max.z
end
end
new_vel.z = 0
end
return new_pos, new_vel
end
function Player:init()
self.aabb = AABB.new(Vector3(-0.25, 0, -0.25), Vector3(0.5, 1.1, 0.5))
end
function Player:tick(ctx)
input_action{name = "left", control = "A"}
input_action{name = "right", control = "D"}
input_action{name = "forward", control = "W"}
input_action{name = "back", control = "S"}
input_action{name = "throw", control = "SPACE"}
input_action{name = "throw", control = "LCLICK"}
local camera_forward = Vector3(draw_camera_from_principal_axes(self).direction)
camera_forward.y = 0
@ -32,17 +106,40 @@ function Player:tick(ctx)
local strafe_input = util.b2n(input_action_pressed{name = "right"}) - util.b2n(input_action_pressed{name = "left"})
local direction = ((camera_forward * forward_input) + (camera_right * strafe_input)):normalized()
self.velocity = direction * self.speed
local target_vel = direction * self.speed
-- self.velocity = self.velocity:lerp(target_vel, self.accel)
self.velocity = target_vel:copy()
self.position = self.position + self.velocity
if self.aabb:intersects(self.test_intersect) then
local new_pos, new_vel = resolve_collision(self.aabb, self.test_intersect, self.velocity)
self.position:set(
new_pos.x - aabb_offset.x,
new_pos.y - aabb_offset.y,
new_pos.z - aabb_offset.z
)
self.aabb.min:set(
self.position.x + aabb_offset.x,
self.position.y + aabb_offset.y,
self.position.z + aabb_offset.z
)
self.velocity:sett(new_vel)
end
if input_action_just_pressed{name = "throw"} then
self.ThrowPressed:emit(self.position:copy(), camera_forward:copy())
end
-- self.aabb.min = self.position + aabb_offset
self.aabb.min:set(
self.position.x + aabb_offset.x,
self.position.y + aabb_offset.y,
self.position.z + aabb_offset.z
)
if ctx.mouse_capture then
self.yaw = self.yaw + self.mouse_sensitivity * ctx.mouse_movement.x
end
self.position = self.position + self.velocity
self.aabb:draw()
end
return Player

View File

@ -1,23 +1,75 @@
util = require "util"
local player = require "classes.player"
local util = require "util"
local Vector3 = require "types.vector3"
local Feed = require "classes.feed"
local List = require "types.list"
local AABB = require "types.aabb"
local feed = {}
local test_aabb = AABB.new(
Vector3(-1, 0, 3),
Vector3(1, 2, 1)
)
local Obj = require "classes.obj"
local cube = Obj.create("models/unit_cube.obj", {file = "images/measure002a.png", size = 512}, Vector3(0, 1, 1))
local Feed = require "classes.feed"
---@type List
local feed = List()
local Duck = require "classes.duck"
---@type List
local ducks = List()
local function create_feed(position, direction)
table.insert(feed, Feed.new(position, direction))
local f = Feed.new(position, direction)
feed:push(f)
end
local function delete_feed(f)
feed:remove_value(f)
end
local function duck_seek_feed(duck)
local eligible_feeds = feed:filter(
function (f)
return f.occupied == false
end
)
if eligible_feeds:is_empty() then return end
duck:start_chase(
eligible_feeds:sorted(
function(a, b)
return a.position:distance_squared_to(duck.position) < b.position:distance_squared_to(duck.position)
end
):pop_front()
)
end
-- called every frame, with constant delta time
function game_tick()
-- ctx.initialization_needed is true first frame and every time dynamic reload is performed
if ctx.initialization_needed then
-- ctx.udata persists on reload
player:init()
player.test_intersect = test_aabb
-- audio_play{audio = "music/bg1.xm", loops = true, channel = "music"}
player.ThrowPressed:connect(create_feed)
-- spawn some ducks
for i = 1, 5 do
local duck = Duck.new(Vector3(0, 0, -math.random() * 10.0):rotated(Vector3.UP, util.random_float(-math.pi, math.pi)))
ducks:push(duck)
duck.AteFeed:connect(delete_feed)
duck.SeekFeed:connect(duck_seek_feed)
duck.index = i
end
end
-- ctx.udata persists on reload
if ctx.udata == nil then
ctx.udata = {
capture = false,
}
player.ThrowPressed:connect(create_feed)
end
ctx.mouse_capture = ctx.udata.capture
@ -27,17 +79,26 @@ function game_tick()
ctx.udata.capture = not ctx.udata.capture
end
for _, v in pairs(feed) do
for _, v in ipairs(feed) do
v:tick(ctx)
end
for _, v in ipairs(ducks) do
v:tick(ctx)
end
player:tick(ctx)
-- draw_camera{position = Vector3(0, 1, 0), direction = Vector3.FORWARD}
-- draw ground
local q = util.create_plane_quad(Vector3(0, 0, 0), Vector3.UP, 20)
local params = {
texture = "images/measure001a.png",
texture_region = { x = 0, y = 0, w = 512, h = 512 },
}
draw_quad(util.merge(q, params))
-- draw_quad(util.merge(q, params))
cube.position.y = math.sin(ctx.frame_number * 0.05)
-- cube.position.z = math.cos(ctx.frame_number * 0.01)
cube.rotation.x = cube.rotation.x + 0.01
cube.rotation.z = cube.rotation.z + 0.01
cube:draw()
test_aabb:draw()
end

View File

@ -0,0 +1,85 @@
local Vector3 = require "types.vector3"
---@class AABB
local AABB = {
min = Vector3(),
size = Vector3(),
}
local RED = {
r = 255,
g = 0,
b = 0,
a = 255,
}
setmetatable(AABB, AABB)
AABB.__index = AABB
---@param position Vector3
---@param size Vector3
---@return AABB
function AABB.new(position, size)
position = position or Vector3()
size = size or Vector3(1, 1, 1)
local aabb = {
min = position,
size = size,
}
return setmetatable(aabb, AABB)
end
function AABB:get_max()
return self.min + self.size
end
function AABB:draw()
local max = self:get_max()
-- bottom rectangle
draw_line_3d{start = self.min, finish = Vector3(max.x, self.min.y, self.min.z)}
draw_line_3d{start = self.min, finish = Vector3(self.min.x, self.min.y, max.z)}
draw_line_3d{start = Vector3(max.x, self.min.y, max.z), finish = Vector3(self.min.x, self.min.y, max.z)}
draw_line_3d{start = Vector3(max.x, self.min.y, max.z), finish = Vector3(max.x, self.min.y, self.min.z)}
-- bottom rectangle diagonal
draw_line_3d{start = self.min, finish = Vector3(max.x, self.min.y, max.z), color = RED}
-- top rectangle
draw_line_3d{start = Vector3(self.min.x, max.y, self.min.z), finish = Vector3(max.x, max.y, self.min.z)}
draw_line_3d{start = Vector3(self.min.x, max.y, self.min.z), finish = Vector3(self.min.x, max.y, max.z)}
draw_line_3d{start = Vector3(max.x, max.y, max.z), finish = Vector3(self.min.x, max.y, max.z)}
draw_line_3d{start = Vector3(max.x, max.y, max.z), finish = Vector3(max.x, max.y, self.min.z)}
-- top rectangle diagonal
draw_line_3d{start = Vector3(max.x, max.y, max.z), finish = Vector3(self.min.x, max.y, self.min.z), color = RED}
-- hull
draw_line_3d{start = self.min, finish = Vector3(self.min.x, max.y, self.min.z)}
draw_line_3d{start = self.min, finish = Vector3(max.x, max.y, self.min.z), color = RED}
draw_line_3d{start = Vector3(max.x, self.min.y, self.min.z), finish = Vector3(max.x, max.y, self.min.z)}
draw_line_3d{start = Vector3(max.x, self.min.y, self.min.z), finish = Vector3(max.x, max.y, max.z), color = RED}
draw_line_3d{start = Vector3(max.x, self.min.y, max.z), finish = Vector3(max.x, max.y, max.z)}
draw_line_3d{start = Vector3(max.x, self.min.y, max.z), finish = Vector3(self.min.x, max.y, max.z), color = RED}
draw_line_3d{start = Vector3(self.min.x, self.min.y, max.z), finish = Vector3(self.min.x, max.y, max.z)}
draw_line_3d{start = Vector3(self.min.x, self.min.y, max.z), finish = Vector3(self.min.x, max.y, self.min.z), color = RED}
end
---returns true if the point is inside this aabb
---@param point Vector3
---@return boolean
function AABB:has_point(point)
local max = self:get_max()
return point > self.min and point < max
end
---returns true if `other` intersects this AABB
---@param other AABB
---@return boolean
function AABB:intersects(other)
local my_max = self:get_max()
local other_max = other:get_max()
return self.min <= other_max and my_max >= other.min
end
return AABB

165
data/scripts/types/list.lua Normal file
View File

@ -0,0 +1,165 @@
---@class List
local List = {}
local function reduce(list, f, init)
local acc = init
for i, v in ipairs(list) do
acc = f(v, acc, i)
end
return acc
end
local function filter(list, predicate)
return reduce(list, function(el, acc, i)
if predicate(el) then
table.insert(acc, el)
end
return acc
end, List.create{})
end
local function shallow_copy(t)
local t2 = {}
for k, v in ipairs(t) do
t2[k] = v
end
return t2
end
List.__index = List
setmetatable(List, {
__call = function(self, ...)
local args = {...}
if #args == 0 then
return List.create()
end
if type(args[1]) == "table" then
return List.create(args[1])
end
return List.create(args)
end
})
---Constructs a new list from the given table.
---@param from table?
---@return List
function List.create(from)
from = from or {}
local l = shallow_copy(from)
return setmetatable(l, List)
end
function List:__tostring()
local s = "List(" .. table.concat(self, ", ", 1, #self) .. ")"
return s
end
---Appends value to the end of the list.
---@param value any
function List:push(value)
if self:has(value) then return end
table.insert(self, value)
end
---Removes the last element in the list and returns it.
---@return any
function List:pop()
return table.remove(self, #self)
end
---Removes the first element in the list and returns it.
---@return any
function List:pop_front()
return table.remove(self, 1)
end
---Reduce.
---@generic T
---@param f fun(element: any, accumulator: T, index: integer): T
---@param init T|nil initial value of accumulator
---@return T
function List:reduce(f, init)
return reduce(self, f, init)
end
---Returns a new List of all elements of this list that match the predicate function.
---@param predicate fun(element: any): boolean
---@return List
function List:filter(predicate)
return filter(self, predicate)
end
---Removes the value at the given index.
---@param idx integer
---@return any
function List:remove_at(idx)
return table.remove(self, idx)
end
---Returns the index of value, if it exists in the list, -1 otherwise.
---@param value any
---@return integer
function List:find(value)
local idx = -1
for i, v in ipairs(self) do
if v == value then
idx = i
break
end
end
return idx
end
---Returns true if the value exists in the list.
---@param value any
---@return boolean
function List:has(value)
return self:find(value) ~= -1
end
---Removes the value from the list, if it exists.
---@param value any
function List:remove_value(value)
local idx = self:find(value)
if idx ~= -1 then
table.remove(self, idx)
end
end
---Returns true if the list is empty.
---@return boolean
function List:is_empty()
return #self == 0
end
---Returns a (shallow) copy of this list.
---@return List
function List:copy()
return List.create(self)
end
---Returns a new copy of this list with the order of the elements shuffled around.
---@return List
function List:shuffled()
-- https://gist.github.com/Uradamus/10323382
local list = self:copy()
for i = #list, 2, -1 do
local j = math.random(i)
list[i], list[j] = list[j], list[i]
end
return list
end
---Returns a sorted copy of this list.
---@param f? fun(a: any, b: any): boolean
---@return List
function List:sorted(f)
local list = self:copy()
table.sort(list, f)
return list
end
return List

View File

@ -1,12 +1,19 @@
---@class Signal
local Signal = {
_connections = {}
}
local util = require "util"
---@field private _connections table
local Signal = {}
Signal.__index = Signal
---Constructs a new signal.
---@return Signal
function Signal.new()
local s = {
_connections = {},
}
return setmetatable(s, Signal)
end
---Connects f to this signal. When the signal is emmitted, this function will be called.
---@param f function
function Signal:connect(f)
@ -28,14 +35,4 @@ function Signal:emit(...)
end
end
---Constructs a new signal.
---@return Signal
function Signal.new()
local s = {
_connections = {},
}
return setmetatable(s, Signal)
end
return Signal

View File

@ -1,8 +1,5 @@
--- @class Vector3
--- @field x number
--- @field y number
--- @field z number
--- @alias vectorlike number[] | Vector3
--- @class Vector3
local Vector3 = {
x = 0,
y = 0,
@ -27,6 +24,25 @@ local function is_weak_vector3(t)
end
end
---Coerces t to a Vector3. If t is already a Vector3, returns it without copying.
---@param t vectorlike
---@param do_err boolean?
---@param def Vector3?
---@return boolean error, Vector3 result
local function coerce(t, do_err, def)
do_err = do_err or true
if do_err and not is_weak_vector3(t) then
def = def or Vector3()
error("Vector3: can not coerce t into Vector3. Returning " .. tostring(def))
return true, def
end
if t["_CLASS_"] == "Vector3" then
return false, t
end
return false, Vector3(t)
end
---Returns a Vector3 multiplied either component-wise (if b is a weak Vector3) or multiplies each component by b if b is a number.
---@param b number|vectorlike
---@return Vector3
@ -139,6 +155,20 @@ function Vector3:__tostring()
return "Vector3(" .. tostring(self.x) .. ", " .. tostring(self.y) .. ", " .. tostring(self.z) .. ")"
end
-- note: the < and <= operators of this class are component-wise rather than lexicographic (that is, htey are useful for bounds checking but are not suitable for sorting.)
function Vector3:__lt(b)
local err, other = coerce(b, true)
if err then return nil end
return self.x < other.x and self.y < other.y and self.z < other.z
end
function Vector3:__le(b)
local err, other = coerce(b, true)
if err then return nil end
return self.x <= other.x and self.y <= other.y and self.z <= other.z
end
Vector3.__index = Vector3
--------API--------
@ -167,12 +197,11 @@ end
---@param with vectorlike
---@return number
function Vector3:dot(with)
if not is_weak_vector3(with) then
error("Vector3: with must be a Vector3-like table. Returning 0")
return 0
local err, v2 = coerce(with)
if err then
return 0.0
end
local v2 = Vector3(with)
return self.x * v2.x + self.y * v2.y + self.z * v2.z
end
@ -180,11 +209,10 @@ end
---@param with vectorlike
---@return Vector3
function Vector3:cross(with)
if not is_weak_vector3(with) then
error("Vector3: with must be a Vector3-like table. Returning Vector3()")
return Vector3()
local err, v2 = coerce(with)
if err then
return v2
end
local v2 = Vector3(with)
return Vector3 {
self.y * v2.z - self.z * v2.y,
self.z * v2.x - self.x * v2.z,
@ -197,18 +225,57 @@ end
---@param angle number
---@return Vector3
function Vector3:rotated(axis, angle)
if not is_weak_vector3(axis) then
error("Vector3: axis must be a Vector3-like table. Returning Vector3()")
return Vector3()
-- if not is_weak_vector3(axis) then
-- error("Vector3: axis must be a Vector3-like table. Returning Vector3()")
-- return Vector3()
-- end
local err, vaxis = coerce(axis)
if err then
return vaxis
end
axis = Vector3(axis):normalized()
vaxis = vaxis:normalized()
local cosa = math.cos(angle)
local sina = math.sin(angle)
-- __mul is only defined for the left operand (table), numbers don't get metatables.
-- as such, the ordering of operations here is specific
local v = (self * cosa) + (axis * ((1 - cosa) * self:dot(axis))) + (axis:cross(self) * sina)
return Vector3(v)
return (self * cosa) + (vaxis * ((1 - cosa) * self:dot(vaxis))) + (vaxis:cross(self) * sina)
end
---In-place version of rotated.
---@param axis Vector3
---@param angle number
---@return Vector3
function Vector3:rotate(axis, angle)
local cosa = math.cos(angle)
local sina = math.sin(angle)
local dot = self:dot(axis)
local cross = axis:cross(self)
self.x = self.x * cosa + axis.x * ((1 - cosa) * dot) + cross.x * sina
self.y = self.y * cosa + axis.y * ((1 - cosa) * dot) + cross.y * sina
self.z = self.z * cosa + axis.z * ((1 - cosa) * dot) + cross.z * sina
return self
end
---In place set.
---@param x number
---@param y number
---@param z number
---@return Vector3
function Vector3:set(x, y, z)
self.x, self.y, self.z = x, y, z
return self
end
---In place set.
---@param t vectorlike
---@return Vector3
function Vector3:sett(t)
self.x, self.y, self.z = t.x, t.y, t.z
return self
end
---Returns a copy of this vector.
@ -216,14 +283,76 @@ end
function Vector3:copy()
return Vector3(self)
end
---Returns a new vector as a linear interpolation between self and b.
---@param b vectorlike
---@param t number
---@return Vector3
function Vector3:lerp(b, t)
local err, w = coerce(b)
if err then
return w
end
return Vector3{
util.lerp(self.x, w.x, t),
util.lerp(self.y, w.y, t),
util.lerp(self.z, w.z, t),
}
end
function Vector3:direction_to(to)
local err, other = coerce(to)
if err then
return 0.0
end
return Vector3(other.x - self.x, other.y - self.y, other.z - self.z):normalized()
end
---Returns the squared distance between this vector and to.
---@param to vectorlike
---@return number
function Vector3:distance_squared_to(to)
local err, other = coerce(to)
if err then
return 0.0
end
return (other - self):length_squared()
end
---Returns the distance between this vector and to.
---@param to vectorlike
---@return number
function Vector3:distance_to(to)
local err, other = coerce(to)
if err then
return 0.0
end
return (other - self):length()
end
---Returns a new vector with the Y discarded (0).
---@return Vector3
function Vector3:horizontal()
return Vector3(self.x, 0.0, self.z)
end
---- CONSTANTS
---@type Vector3
Vector3.UP = Vector3(0, 1, 0)
Vector3.DOWN = -Vector3.UP
---@type Vector3
Vector3.DOWN = Vector3(0, -1, 0)
---@type Vector3
Vector3.FORWARD = Vector3(0, 0, -1)
Vector3.BACK = -Vector3.FORWARD
---@type Vector3
Vector3.BACK = Vector3(0, 0, 1)
---@type Vector3
Vector3.RIGHT = Vector3(1, 0, 0)
Vector3.LEFT = -Vector3.RIGHT
---@type Vector3
Vector3.LEFT = Vector3(-1, 0, 0)
-------------------
return Vector3

View File

@ -97,4 +97,32 @@ function util.list_insert_once(t, value)
if util.list_find(t, value) ~= -1 then return end
table.insert(t, value)
end
---linear interpolation
---@param a number
---@param b number
---@param t number in [0, 1]
---@return number
function util.lerp(a, b, t)
return a + t * (b - a)
end
---constrains v between min and max
---@param v number
---@param min number
---@param max number
---@return number
function util.clamp(v, min, max)
return math.max(math.min(v, max), min)
end
---returns a random float in range (min, max)
---@param min number
---@param max number
---@return number
function util.random_float(min, max)
return min + math.random() * (max - min)
end
return util