Compare commits

...

13 Commits

10 changed files with 494 additions and 36 deletions

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

@ -99,7 +99,7 @@ end
function Duck:start_chase(feed) function Duck:start_chase(feed)
if self.state == States.CHASE then return end if self.state == States.CHASE then return end
print("duck " .. self.index .. " starting chase") -- print("duck " .. self.index .. " starting chase")
self.state = States.CHASE self.state = States.CHASE
self.target = feed self.target = feed
feed.occupied = true feed.occupied = true

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,5 +1,6 @@
local Vector3 = require("types.vector3") local Vector3 = require("types.vector3")
local Signal = require("types.signal") local Signal = require("types.signal")
local AABB = require("types.aabb")
local Player = { local Player = {
position = Vector3(0, 1, 0), position = Vector3(0, 1, 0),
@ -12,10 +13,82 @@ local Player = {
yaw_speed = 0.05, yaw_speed = 0.05,
accel = 0.3, accel = 0.3,
aabb = nil,
test_intersect = nil,
ThrowPressed = Signal.new(), 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) function Player:tick(ctx)
input_action{name = "left", control = "A"} input_action{name = "left", control = "A"}
input_action{name = "right", control = "D"} input_action{name = "right", control = "D"}
@ -34,17 +107,39 @@ function Player:tick(ctx)
local direction = ((camera_forward * forward_input) + (camera_right * strafe_input)):normalized() local direction = ((camera_forward * forward_input) + (camera_right * strafe_input)):normalized()
local target_vel = direction * self.speed local target_vel = direction * self.speed
self.velocity = self.velocity:lerp(target_vel, self.accel) -- 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 if input_action_just_pressed{name = "throw"} then
self.ThrowPressed:emit(self.position:copy(), camera_forward:copy()) self.ThrowPressed:emit(self.position:copy(), camera_forward:copy())
end 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 if ctx.mouse_capture then
self.yaw = self.yaw + self.mouse_sensitivity * ctx.mouse_movement.x self.yaw = self.yaw + self.mouse_sensitivity * ctx.mouse_movement.x
end end
self.position = self.position + self.velocity
self.aabb:draw()
end end
return Player return Player

View File

@ -2,24 +2,28 @@ util = require "util"
local player = require "classes.player" local player = require "classes.player"
local Vector3 = require "types.vector3" local Vector3 = require "types.vector3"
local List = require "types.list" local List = require "types.list"
local AABB = require "types.aabb"
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" local Feed = require "classes.feed"
---@type List
local feed = List() local feed = List()
local Duck = require "classes.duck" local Duck = require "classes.duck"
---@type List
local ducks = List() local ducks = List()
local function create_feed(position, direction) local function create_feed(position, direction)
print("?")
local f = Feed.new(position, direction) local f = Feed.new(position, direction)
feed:push(f) feed:push(f)
local eligible_ducks = ducks:filter(
function (duck)
return duck.state ~= duck.STATES.CHASE
end
)
if eligible_ducks:is_empty() then return end
eligible_ducks[1]:start_chase(f)
end end
local function delete_feed(f) local function delete_feed(f)
@ -29,21 +33,26 @@ end
local function duck_seek_feed(duck) local function duck_seek_feed(duck)
local eligible_feeds = feed:filter( local eligible_feeds = feed:filter(
function (f) function (f)
return feed.occupied == false return f.occupied == false
end end
) )
if eligible_feeds:is_empty() then return end if eligible_feeds:is_empty() then return end
duck:start_chase(eligible_feeds[1]) 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 end
-- called every frame, with constant delta time -- called every frame, with constant delta time
function game_tick() function game_tick()
-- ctx.initialization_needed is true first frame and every time dynamic reload is performed -- ctx.initialization_needed is true first frame and every time dynamic reload is performed
if ctx.initialization_needed then if ctx.initialization_needed then
-- ctx.udata persists on reload player:init()
ctx.udata = { player.test_intersect = test_aabb
capture = false, -- audio_play{audio = "music/bg1.xm", loops = true, channel = "music"}
}
player.ThrowPressed:connect(create_feed) player.ThrowPressed:connect(create_feed)
-- spawn some ducks -- spawn some ducks
@ -54,9 +63,13 @@ function game_tick()
duck.SeekFeed:connect(duck_seek_feed) duck.SeekFeed:connect(duck_seek_feed)
duck.index = i duck.index = i
end end
print(ducks[1].AteFeed._connections) end
print(ducks[2].AteFeed._connections)
-- ctx.udata persists on reload
if ctx.udata == nil then
ctx.udata = {
capture = false,
}
end end
ctx.mouse_capture = ctx.udata.capture ctx.mouse_capture = ctx.udata.capture
@ -80,5 +93,12 @@ function game_tick()
texture = "images/measure001a.png", texture = "images/measure001a.png",
texture_region = { x = 0, y = 0, w = 512, h = 512 }, 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 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

View File

@ -68,16 +68,23 @@ function List:pop()
return table.remove(self, #self) return table.remove(self, #self)
end end
---Reduce. ---Removes the first element in the list and returns it.
---@param f function called with element, accumulator, index
---@param init any initial value of accumulator
---@return any ---@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) function List:reduce(f, init)
return reduce(self, f, init) return reduce(self, f, init)
end end
---Returns a new List of all elements of this list that match the predicate function. ---Returns a new List of all elements of this list that match the predicate function.
---@param predicate function called with element ---@param predicate fun(element: any): boolean
---@return List ---@return List
function List:filter(predicate) function List:filter(predicate)
return filter(self, predicate) return filter(self, predicate)
@ -126,4 +133,33 @@ function List:is_empty()
return #self == 0 return #self == 0
end 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 return List

View File

@ -1,8 +1,5 @@
--- @class Vector3
--- @field x number
--- @field y number
--- @field z number
--- @alias vectorlike number[] | Vector3 --- @alias vectorlike number[] | Vector3
--- @class Vector3
local Vector3 = { local Vector3 = {
x = 0, x = 0,
y = 0, y = 0,
@ -158,6 +155,20 @@ function Vector3:__tostring()
return "Vector3(" .. tostring(self.x) .. ", " .. tostring(self.y) .. ", " .. tostring(self.z) .. ")" return "Vector3(" .. tostring(self.x) .. ", " .. tostring(self.y) .. ", " .. tostring(self.z) .. ")"
end 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 Vector3.__index = Vector3
--------API-------- --------API--------
@ -223,14 +234,48 @@ function Vector3:rotated(axis, angle)
return vaxis return vaxis
end end
---@diagnostic disable-next-line: undefined-field
vaxis = vaxis:normalized() vaxis = vaxis:normalized()
local cosa = math.cos(angle) local cosa = math.cos(angle)
local sina = math.sin(angle) local sina = math.sin(angle)
-- __mul is only defined for the left operand (table), numbers don't get metatables. -- __mul is only defined for the left operand (table), numbers don't get metatables.
-- as such, the ordering of operations here is specific -- as such, the ordering of operations here is specific
local v = (self * cosa) + (vaxis * ((1 - cosa) * self:dot(vaxis))) + (vaxis:cross(self) * sina) return (self * cosa) + (vaxis * ((1 - cosa) * self:dot(vaxis))) + (vaxis:cross(self) * sina)
return Vector3(v) 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 end
---Returns a copy of this vector. ---Returns a copy of this vector.
@ -296,12 +341,18 @@ function Vector3:horizontal()
end end
---- CONSTANTS ---- CONSTANTS
---@type Vector3
Vector3.UP = Vector3(0, 1, 0) 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.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.RIGHT = Vector3(1, 0, 0)
Vector3.LEFT = -Vector3.RIGHT ---@type Vector3
Vector3.LEFT = Vector3(-1, 0, 0)
------------------- -------------------
return Vector3 return Vector3