mirror of
https://github.com/Ukendio/jecs.git
synced 2026-02-04 15:15:21 +00:00
New modules with examples!
This commit is contained in:
parent
2f2ce3541d
commit
a2e6c0aafa
13 changed files with 1039 additions and 1 deletions
|
|
@ -134,7 +134,7 @@ local function networking_send(world: jecs.World)
|
||||||
set_n += entities_len
|
set_n += entities_len
|
||||||
end
|
end
|
||||||
|
|
||||||
local set = table.move(set_ids_lazy, 1, set_n, 1, {})
|
local set = table.move(set_ids_lazy, 1, set_n, 1, table.create(set_n))
|
||||||
|
|
||||||
local map = {
|
local map = {
|
||||||
set = if set_n > 0 then set else nil,
|
set = if set_n > 0 then set else nil,
|
||||||
|
|
|
||||||
44
modules/BT/module.luau
Executable file
44
modules/BT/module.luau
Executable file
|
|
@ -0,0 +1,44 @@
|
||||||
|
-- original author @centau
|
||||||
|
|
||||||
|
local SUCCESS = true
|
||||||
|
local FAILURE = false
|
||||||
|
local RUNNING = newproxy(false)
|
||||||
|
|
||||||
|
local function SEQUENCE(nodes: { (...any) -> boolean})
|
||||||
|
return function(...)
|
||||||
|
for _, node in nodes do
|
||||||
|
local status = node(...)
|
||||||
|
if not status or status == RUNNING then
|
||||||
|
return status
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return SUCCESS
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function FALLBACK(nodes: { (...any) -> boolean })
|
||||||
|
return function(...)
|
||||||
|
for _, node in nodes do
|
||||||
|
local status = node(...)
|
||||||
|
if status or status == RUNNING then
|
||||||
|
return status
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return FAILURE
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function NOT(f)
|
||||||
|
return function(...): boolean
|
||||||
|
return not f(...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local bt = {
|
||||||
|
SEQUENCE = SEQUENCE,
|
||||||
|
FALLBACK = FALLBACK,
|
||||||
|
RUNNING = RUNNING,
|
||||||
|
NOT = NOT,
|
||||||
|
}
|
||||||
|
|
||||||
|
return bt
|
||||||
32
modules/GetRect/examples/button.luau
Executable file
32
modules/GetRect/examples/button.luau
Executable file
|
|
@ -0,0 +1,32 @@
|
||||||
|
local ReplicatedStorage = require("@game/ReplicatedStorage")
|
||||||
|
local RunService = require("@game/RunService")
|
||||||
|
local UI_Context = require(ReplicatedStorage.UI_Context)
|
||||||
|
local GetRect = require("@modules/GetRect/module")
|
||||||
|
|
||||||
|
RunService.RenderStepped:Connect(fúnction(dt)
|
||||||
|
local pressed, button_state, released = GetRect.button(UI_Context.button.shop)
|
||||||
|
|
||||||
|
local my_window = UI_Context.window.shop
|
||||||
|
local state = UI_Context.state
|
||||||
|
if released then
|
||||||
|
my_window.Visible = not my_window.shop.Visible
|
||||||
|
state.pop_effect_t = 0
|
||||||
|
state.pop_duration += dt
|
||||||
|
end
|
||||||
|
|
||||||
|
state.pop_effect_t = GetRect.animate(state.pop_effect_t, my_window.shop_visible, dt * 10)
|
||||||
|
local base = 0.6
|
||||||
|
local blend_factor = math.sin(math.pi/2*state.pop_duration * 0.5)
|
||||||
|
blend_factor += 1
|
||||||
|
blend_factor *= 0.5
|
||||||
|
state.pop_factor = 0.6 + state.pop_effect_t * blend_factor
|
||||||
|
|
||||||
|
local color3 = my_window.Title.TextColor3
|
||||||
|
local r, g, b = color3.R, color3.G, color3.B
|
||||||
|
r = math.lerp(r, UI_Context.theme.text_color_pop, state.pop_factor)
|
||||||
|
g = math.lerp(g, UI_Context.theme.text_color_pop, state.pop_factor)
|
||||||
|
b = math.lerp(b, UI_Context.theme.text_color_pop, state.pop_factor)
|
||||||
|
|
||||||
|
my_window.UIScale = state.pop_effect_t
|
||||||
|
my_window.Title.TextColor3 = Color3.new(r, g, b)
|
||||||
|
end)
|
||||||
108
modules/GetRect/module.luau
Executable file
108
modules/GetRect/module.luau
Executable file
|
|
@ -0,0 +1,108 @@
|
||||||
|
local state_table = {} :: {[GuiObject]: Active_Widget }
|
||||||
|
type Active_Widget = {
|
||||||
|
last_update_time: number,
|
||||||
|
is_on_state_stack: boolean,
|
||||||
|
last_getrect_frame: number,
|
||||||
|
instance: GuiObject,
|
||||||
|
parent: Instance
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
local function orphan<T>(template: T): T
|
||||||
|
-- NOTE(marcus): Right now this does nothing...
|
||||||
|
local instance = template :: Instance
|
||||||
|
if instance then
|
||||||
|
return template
|
||||||
|
end
|
||||||
|
instance.Parent = nil
|
||||||
|
return instance:Clone() :: T
|
||||||
|
end
|
||||||
|
|
||||||
|
local function find_or_create_state<T>(rect: GuiObject): (T, boolean)
|
||||||
|
local state = state_table[rect]
|
||||||
|
if state then
|
||||||
|
return state, false
|
||||||
|
end
|
||||||
|
|
||||||
|
state = {
|
||||||
|
last_getrect_frame = -1,
|
||||||
|
last_update_time = -1,
|
||||||
|
is_on_state_stack = false,
|
||||||
|
instance = orphan(rect) -- NOTE(marcus): In the future we could just as easily treat rect as a theme and then just clone the instance by orphaning it
|
||||||
|
} :: Active_Widget
|
||||||
|
state_table[rect] = state
|
||||||
|
|
||||||
|
return state, true
|
||||||
|
end
|
||||||
|
|
||||||
|
type Button_State = {
|
||||||
|
pressed: boolean?,
|
||||||
|
prev: boolean?,
|
||||||
|
down: boolean?,
|
||||||
|
released: boolean?,
|
||||||
|
status: Enum.GuiState,
|
||||||
|
} & Active_Widget
|
||||||
|
|
||||||
|
local function button(rect: GuiObject): (boolean, Button_State, boolean)
|
||||||
|
|
||||||
|
local state = find_or_create_state(rect) :: Button_State
|
||||||
|
local status = state.instance.GuiState
|
||||||
|
state.status = status
|
||||||
|
|
||||||
|
local released = false
|
||||||
|
local result = false
|
||||||
|
|
||||||
|
if state.pressed then
|
||||||
|
if not (status == Enum.GuiState.Press) then
|
||||||
|
released = true
|
||||||
|
state.pressed = false
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if status == Enum.GuiState.Press then
|
||||||
|
state.pressed = true
|
||||||
|
result = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if state.pressed then
|
||||||
|
state.status = Enum.GuiState.Hover
|
||||||
|
end
|
||||||
|
|
||||||
|
return result, state, released
|
||||||
|
end
|
||||||
|
|
||||||
|
local function move_toward(a: number, b: number, amount_increasing: number, amount_decreasing: number?)
|
||||||
|
amount_decreasing = amount_decreasing or -1
|
||||||
|
if a > b then
|
||||||
|
if amount_decreasing == -1 then
|
||||||
|
amount_decreasing = amount_increasing
|
||||||
|
end
|
||||||
|
a -= amount_decreasing
|
||||||
|
if a < b then
|
||||||
|
a = b
|
||||||
|
end
|
||||||
|
else
|
||||||
|
a += amount_increasing
|
||||||
|
if a > b then
|
||||||
|
a = b
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return a
|
||||||
|
end
|
||||||
|
|
||||||
|
local function animate(f: number, condition: boolean, dt: number, up_rate: number, down_rate: number)
|
||||||
|
if condition then
|
||||||
|
return move_toward(f, 1, dt * up_rate)
|
||||||
|
else
|
||||||
|
return move_toward(f, 0, dt * down_rate)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local GetRect = {
|
||||||
|
button = button,
|
||||||
|
move_toward = move_toward,
|
||||||
|
animate = animate,
|
||||||
|
orphan = orphan
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetRect
|
||||||
69
modules/OB/examples/example.luau
Executable file
69
modules/OB/examples/example.luau
Executable file
|
|
@ -0,0 +1,69 @@
|
||||||
|
local jecs = require("@jecs")
|
||||||
|
local OB = require("@modules/OB/module")
|
||||||
|
|
||||||
|
local ct = {
|
||||||
|
Transform = jecs.component() :: jecs.Id<CFrame>,
|
||||||
|
PivotTo = jecs.component() :: jecs.Id<CFrame>,
|
||||||
|
TRANSFORM_PREDICTED = jecs.tag(),
|
||||||
|
Renderable = jecs.component() :: jecs.Id<Instance>
|
||||||
|
}
|
||||||
|
|
||||||
|
local function main(world: jecs.World)
|
||||||
|
local function pivots_added(entity: jecs.Entity)
|
||||||
|
local dst: CFrame?, model: Instance? = world:get(entity, ct.PivotTo, ct.Renderable)
|
||||||
|
if dst == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
world:set(entity, ct.Transform, dst)
|
||||||
|
world:remove(entity, ct.PivotTo)
|
||||||
|
;(model :: Model):PivotTo(dst)
|
||||||
|
end
|
||||||
|
|
||||||
|
local authoritative_transforms = world
|
||||||
|
:query(ct.Transform, ct.Renderable)
|
||||||
|
:without(ct.TRANSFORM_PREDICTED)
|
||||||
|
:cached()
|
||||||
|
|
||||||
|
OB.observer(world:query(ct.PivotTo, ct.Renderable), pivots_added)
|
||||||
|
|
||||||
|
return function()
|
||||||
|
for entity, transform, renderable in authoritative_transforms do
|
||||||
|
local model = renderable :: Model
|
||||||
|
local primarypart = model.PrimaryPart
|
||||||
|
if primarypart == nil or primarypart.CFrame == transform then
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
primarypart.CFrame = transform
|
||||||
|
end
|
||||||
|
|
||||||
|
for entity, transform, model in predicted_transforms do
|
||||||
|
local primarypart = (model :: Model).PrimaryPart
|
||||||
|
if primarypart == nil then
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
local cframe = primarypart.CFrame
|
||||||
|
|
||||||
|
if (transform.Position - cframe.Position).Magnitude < 0.1 then
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
world:set(entity, ct.Transform, cframe)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local w = jecs.world()
|
||||||
|
|
||||||
|
local entity = world:entity()
|
||||||
|
local part = Instance.new("Part")
|
||||||
|
part.Anchored = true
|
||||||
|
part.Parent = workspace
|
||||||
|
world:set(entity, ct.Renderable, part)
|
||||||
|
world:set(entity, ct.PivotTo, CFrame.new())
|
||||||
|
|
||||||
|
local system = main(w)
|
||||||
|
system(1/60)
|
||||||
|
|
||||||
187
modules/Spring/cframe.luau
Executable file
187
modules/Spring/cframe.luau
Executable file
|
|
@ -0,0 +1,187 @@
|
||||||
|
--!native
|
||||||
|
--!optimize 2
|
||||||
|
--!strict
|
||||||
|
|
||||||
|
local Spring_Vector3 = require("./vector3")
|
||||||
|
|
||||||
|
export type Spring = {
|
||||||
|
type: "CFrame",
|
||||||
|
position: Spring_Vector3.Spring,
|
||||||
|
rotation: Spring_Rotation,
|
||||||
|
d: number,
|
||||||
|
f: number,
|
||||||
|
g: CFrame,
|
||||||
|
p: CFrame
|
||||||
|
}
|
||||||
|
|
||||||
|
type Spring_Rotation = {
|
||||||
|
d: number,
|
||||||
|
f: number,
|
||||||
|
g: CFrame,
|
||||||
|
p: CFrame,
|
||||||
|
v: Vector3
|
||||||
|
}
|
||||||
|
|
||||||
|
-- evaluate dot products in high precision
|
||||||
|
local function dot(v0: Vector3, v1: Vector3)
|
||||||
|
return v0.X*v1.X + v0.Y*v1.Y + v0.Z*v1.Z
|
||||||
|
end
|
||||||
|
|
||||||
|
local function angleDiff(c0: CFrame, c1: CFrame)
|
||||||
|
local x = dot(c0.XVector, c1.XVector)
|
||||||
|
local y = dot(c0.YVector, c1.YVector)
|
||||||
|
local z = dot(c0.ZVector, c1.ZVector)
|
||||||
|
local w = x + y + z - 1
|
||||||
|
return math.atan2(math.sqrt(math.max(0, 1 - w*w*0.25)), w*0.5)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- gives approx. 21% accuracy improvement over CFrame.fromAxisAngle near poles
|
||||||
|
local function fromAxisAngle(axis: Vector3, angle: number)
|
||||||
|
local c = math.cos(angle)
|
||||||
|
local s = math.sin(angle)
|
||||||
|
local x, y, z = axis.X, axis.Y, axis.Z
|
||||||
|
|
||||||
|
local mxy = x*y*(1 - c)
|
||||||
|
local myz = y*z*(1 - c)
|
||||||
|
local mzx = z*x*(1 - c)
|
||||||
|
|
||||||
|
local rx = Vector3.new(x*x*(1 - c) + c, mxy + z*s, mzx - y*s)
|
||||||
|
local ry = Vector3.new(mxy - z*s, y*y*(1 - c) + c, myz + x*s)
|
||||||
|
local rz = Vector3.new(mzx + y*s, myz - x*s, z*z*(1 - c) + c)
|
||||||
|
|
||||||
|
return CFrame.fromMatrix(Vector3.zero, rx, ry, rz):Orthonormalize()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function rotateAxis(r0: Vector3, c1: CFrame)
|
||||||
|
local c0 = CFrame.identity
|
||||||
|
local mag = r0.Magnitude
|
||||||
|
if mag > 1e-6 then
|
||||||
|
c0 = fromAxisAngle(r0.Unit, mag)
|
||||||
|
end
|
||||||
|
return c0 * c1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- axis*angle difference between two cframes
|
||||||
|
local function axisAngleDiff(c0: CFrame, c1: CFrame)
|
||||||
|
-- use native axis (stable enough)
|
||||||
|
local axis = (c0*c1:Inverse()):ToAxisAngle()
|
||||||
|
|
||||||
|
-- use full-precision angle calculation to minimize truncation
|
||||||
|
local angle = angleDiff(c0, c1)
|
||||||
|
return axis.Unit*angle
|
||||||
|
end
|
||||||
|
|
||||||
|
local function rotation_step(spring: Spring_Rotation, dt: number): CFrame
|
||||||
|
debug.profilebegin("rotation")
|
||||||
|
local d = spring.d
|
||||||
|
local f = spring.f*(2*math.pi)
|
||||||
|
local g = spring.g
|
||||||
|
local p0 = spring.p
|
||||||
|
local v0 = spring.v
|
||||||
|
|
||||||
|
local offset = axisAngleDiff(p0, g)
|
||||||
|
local decay = math.exp(-d*f*dt)
|
||||||
|
|
||||||
|
local pt: CFrame
|
||||||
|
local vt: Vector3
|
||||||
|
|
||||||
|
if d == 1 then -- critically damped
|
||||||
|
pt = rotateAxis((offset*(1 + f*dt) + v0*dt)*decay, g)
|
||||||
|
vt = (v0*(1 - dt*f) - offset*(dt*f*f))*decay
|
||||||
|
|
||||||
|
elseif d < 1 then -- underdamped
|
||||||
|
local c = math.sqrt(1 - d*d)
|
||||||
|
|
||||||
|
local i = math.cos(dt*f*c)
|
||||||
|
local j = math.sin(dt*f*c)
|
||||||
|
|
||||||
|
local y = j/(f*c)
|
||||||
|
local z = j/c
|
||||||
|
|
||||||
|
pt = rotateAxis((offset*(i + z*d) + v0*y)*decay, g)
|
||||||
|
vt = (v0*(i - z*d) - offset*(z*f))*decay
|
||||||
|
else -- overdamped
|
||||||
|
local c = math.sqrt(d*d - 1)
|
||||||
|
|
||||||
|
local r1 = -f*(d + c)
|
||||||
|
local r2 = -f*(d - c)
|
||||||
|
|
||||||
|
local co2 = (v0 - offset*r1)/(2*f*c)
|
||||||
|
local co1 = offset - co2
|
||||||
|
|
||||||
|
local e1 = co1*math.exp(r1*dt)
|
||||||
|
local e2 = co2*math.exp(r2*dt)
|
||||||
|
|
||||||
|
pt = rotateAxis(e1 + e2, g)
|
||||||
|
vt = e1*r1 + e2*r2
|
||||||
|
end
|
||||||
|
|
||||||
|
spring.p = pt
|
||||||
|
spring.v = vt
|
||||||
|
|
||||||
|
debug.profileend()
|
||||||
|
|
||||||
|
return pt
|
||||||
|
end
|
||||||
|
|
||||||
|
local SLEEP_ROTATION_DIFF = math.rad(0.01) -- rad
|
||||||
|
local SLEEP_ROTATION_VELOCITY = math.rad(0.1) -- rad/s
|
||||||
|
|
||||||
|
local function areRotationsClose(c0: CFrame, c1: CFrame)
|
||||||
|
local rx = dot(c0.XVector, c1.XVector)
|
||||||
|
local ry = dot(c0.YVector, c1.YVector)
|
||||||
|
local rz = dot(c0.ZVector, c1.ZVector)
|
||||||
|
local trace = rx + ry + rz
|
||||||
|
return trace > 1 + 2*math.cos(SLEEP_ROTATION_DIFF)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function rotation_can_sleep(spring: Spring_Rotation): boolean
|
||||||
|
local sleepP = areRotationsClose(spring.p, spring.g)
|
||||||
|
local sleepV = spring.v.Magnitude < SLEEP_ROTATION_VELOCITY
|
||||||
|
return sleepP and sleepV
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Very important to remember that the origo and the goal's Rotation are
|
||||||
|
-- orthonormalized!
|
||||||
|
local function create(d: number, f: number, origo: CFrame, goal: CFrame): Spring
|
||||||
|
return {
|
||||||
|
type = "CFrame",
|
||||||
|
position = {
|
||||||
|
type = "Vector3",
|
||||||
|
d = d,
|
||||||
|
f = f,
|
||||||
|
g = goal.Position,
|
||||||
|
p = origo.Position,
|
||||||
|
v = Vector3.zero,
|
||||||
|
},
|
||||||
|
rotation = {
|
||||||
|
d = d,
|
||||||
|
f = f,
|
||||||
|
g = goal.Rotation:Orthonormalize(),
|
||||||
|
p = origo.Rotation:Orthonormalize(),
|
||||||
|
v = Vector3.zero
|
||||||
|
},
|
||||||
|
d = d,
|
||||||
|
f = f,
|
||||||
|
p = origo,
|
||||||
|
g = goal
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function step(spring: Spring, dt: number): CFrame
|
||||||
|
local p = Spring_Vector3.step(spring.position, dt)
|
||||||
|
local r = rotation_step(spring.rotation, dt)
|
||||||
|
local cframe = r + p
|
||||||
|
spring.p = cframe
|
||||||
|
return cframe
|
||||||
|
end
|
||||||
|
|
||||||
|
local function can_sleep(spring: Spring)
|
||||||
|
return Spring_Vector3.can_sleep(spring.position) and rotation_can_sleep(spring.rotation)
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
create = create,
|
||||||
|
step = step,
|
||||||
|
can_sleep = can_sleep
|
||||||
|
}
|
||||||
110
modules/Spring/color3.luau
Executable file
110
modules/Spring/color3.luau
Executable file
|
|
@ -0,0 +1,110 @@
|
||||||
|
--!native
|
||||||
|
--!optimize 2
|
||||||
|
--!strict
|
||||||
|
local Spring_Vector3 = require("./vector3")
|
||||||
|
|
||||||
|
export type Spring = {
|
||||||
|
type: "Color3",
|
||||||
|
d: number,
|
||||||
|
f: number,
|
||||||
|
g: Vector3,
|
||||||
|
p: Vector3,
|
||||||
|
v: Vector3
|
||||||
|
}
|
||||||
|
|
||||||
|
local function create(d: number, f: number, origo: Color3, goal: Color3): Spring
|
||||||
|
return {
|
||||||
|
type = "Color3",
|
||||||
|
d = d,
|
||||||
|
f = f,
|
||||||
|
g = Vector3.new(),
|
||||||
|
p = Vector3.new(),
|
||||||
|
v = Vector3.new(),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function inverseGammaCorrectD65(c)
|
||||||
|
return c < 0.0404482362771076 and c/12.92 or 0.87941546140213*(c + 0.055)^2.4
|
||||||
|
end
|
||||||
|
|
||||||
|
local function gammaCorrectD65(c)
|
||||||
|
return c < 3.1306684425e-3 and 12.92*c or 1.055*c^(1/2.4) - 0.055
|
||||||
|
end
|
||||||
|
|
||||||
|
local function rgbToLuv(value: Vector3): Color3
|
||||||
|
-- convert RGB to a variant of cieluv space
|
||||||
|
local r, g, b = value.X, value.Y, value.Z
|
||||||
|
|
||||||
|
-- D65 sRGB inverse gamma correction
|
||||||
|
r = inverseGammaCorrectD65(r)
|
||||||
|
g = inverseGammaCorrectD65(g)
|
||||||
|
b = inverseGammaCorrectD65(b)
|
||||||
|
|
||||||
|
-- sRGB -> xyz
|
||||||
|
local x = 0.9257063972951867*r - 0.8333736323779866*g - 0.09209820666085898*b
|
||||||
|
local y = 0.2125862307855956*r + 0.71517030370341085*g + 0.0722004986433362*b
|
||||||
|
local z = 3.6590806972265883*r + 11.4426895800574232*g + 4.1149915024264843*b
|
||||||
|
|
||||||
|
-- xyz -> scaled cieluv
|
||||||
|
local l = y > 0.008856451679035631 and 116*y^(1/3) - 16 or 903.296296296296*y
|
||||||
|
|
||||||
|
local u, v
|
||||||
|
if z > 1e-14 then
|
||||||
|
u = l*x/z
|
||||||
|
v = l*(9*y/z - 0.46832)
|
||||||
|
else
|
||||||
|
u = -0.19783*l
|
||||||
|
v = -0.46832*l
|
||||||
|
end
|
||||||
|
|
||||||
|
return Color3.new(l,u,v)
|
||||||
|
end
|
||||||
|
local function luvToRgb(value: Vector3): Color3
|
||||||
|
-- convert back from modified cieluv to rgb space
|
||||||
|
local l = value.X
|
||||||
|
if l < 0.0197955 then
|
||||||
|
return Color3.new(0, 0, 0)
|
||||||
|
end
|
||||||
|
local u = value.Y/l + 0.19783
|
||||||
|
local v = value.Z/l + 0.46832
|
||||||
|
|
||||||
|
-- cieluv -> xyz
|
||||||
|
local y = (l + 16)/116
|
||||||
|
y = y > 0.206896551724137931 and y*y*y or 0.12841854934601665*y - 0.01771290335807126
|
||||||
|
local x = y*u/v
|
||||||
|
local z = y*((3 - 0.75*u)/v - 5)
|
||||||
|
|
||||||
|
-- xyz -> D65 sRGB
|
||||||
|
local r = 7.2914074*x - 1.5372080*y - 0.4986286*z
|
||||||
|
local g = -2.1800940*x + 1.8757561*y + 0.0415175*z
|
||||||
|
local b = 0.1253477*x - 0.2040211*y + 1.0569959*z
|
||||||
|
|
||||||
|
-- clamp minimum sRGB component
|
||||||
|
if r < 0 and r < g and r < b then
|
||||||
|
r, g, b = 0, g - r, b - r
|
||||||
|
elseif g < 0 and g < b then
|
||||||
|
r, g, b = r - g, 0, b - g
|
||||||
|
elseif b < 0 then
|
||||||
|
r, g, b = r - b, g - b, 0
|
||||||
|
end
|
||||||
|
|
||||||
|
-- gamma correction from D65
|
||||||
|
-- clamp to avoid undesirable overflow wrapping behavior on certain properties (e.g. BasePart.Color)
|
||||||
|
return Color3.new(
|
||||||
|
math.min(gammaCorrectD65(r), 1),
|
||||||
|
math.min(gammaCorrectD65(g), 1),
|
||||||
|
math.min(gammaCorrectD65(b), 1)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function step(spring: Spring, dt: number): Color3
|
||||||
|
Spring_Vector3.step((spring::any), dt)
|
||||||
|
return rgbToLuv(spring.g)
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
create = create,
|
||||||
|
step = step,
|
||||||
|
can_sleep = Spring_Vector3.can_sleep,
|
||||||
|
rgbToLuv = rgbToLuv
|
||||||
|
}
|
||||||
15
modules/Spring/examples/example.luau
Executable file
15
modules/Spring/examples/example.luau
Executable file
|
|
@ -0,0 +1,15 @@
|
||||||
|
local Spring_Vector3 = require("@modules/Spring/vector3")
|
||||||
|
local Spring_Generic = require("@modules/Spring/generic")
|
||||||
|
|
||||||
|
local spring = Spring_Vector3.create(1, 1, vector.create(0, 0, 0), vector.create(1, 1, 1))
|
||||||
|
|
||||||
|
local p = Spring_Vector3.step(spring, 1/60) -- You can also supplement 0 for the deltatime and it will just give you the value it is on right now
|
||||||
|
print(p)
|
||||||
|
print(Spring_Vector3.can_sleep(spring))
|
||||||
|
|
||||||
|
-- This generic spring interface allows you to step any spring which can be
|
||||||
|
-- useful if you would like a single homogenous system to update all scheduled
|
||||||
|
-- springs
|
||||||
|
Spring_Generic.step(spring, 1/60)
|
||||||
|
|
||||||
|
|
||||||
55
modules/Spring/generic.luau
Executable file
55
modules/Spring/generic.luau
Executable file
|
|
@ -0,0 +1,55 @@
|
||||||
|
local cframe = require("./cframe")
|
||||||
|
local color3 = require("./color3")
|
||||||
|
local number = require("./number")
|
||||||
|
local vector2 = require("./vector2")
|
||||||
|
local vector3 = require("./vector3")
|
||||||
|
|
||||||
|
export type Spring =
|
||||||
|
| cframe.Spring
|
||||||
|
| color3.Spring
|
||||||
|
| number.Spring
|
||||||
|
| vector2.Spring
|
||||||
|
| vector3.Spring
|
||||||
|
|
||||||
|
local function exhaustive(x: never)
|
||||||
|
error(x)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function step(spring: Spring, dt: number)
|
||||||
|
if spring.type == "CFrame" then
|
||||||
|
cframe.step(spring, dt)
|
||||||
|
elseif spring.type == "Color3" then
|
||||||
|
color3.step(spring, dt)
|
||||||
|
elseif spring.type == "Vector2" then
|
||||||
|
vector2.step(spring, dt)
|
||||||
|
elseif spring.type == "Vector3" then
|
||||||
|
vector3.step(spring, dt)
|
||||||
|
elseif spring.type == "number" then
|
||||||
|
number.step(spring, dt)
|
||||||
|
else
|
||||||
|
exhaustive(spring.type)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function can_sleep(spring: Spring)
|
||||||
|
if spring.type == "CFrame" then
|
||||||
|
cframe.can_sleep(spring)
|
||||||
|
elseif spring.type == "Color3" then
|
||||||
|
color3.can_sleep((spring::any)::vector3.Spring)
|
||||||
|
elseif spring.type == "Vector2" then
|
||||||
|
vector2.can_sleep(spring)
|
||||||
|
elseif spring.type == "Vector3" then
|
||||||
|
vector3.can_sleep(spring)
|
||||||
|
elseif spring.type == "number" then
|
||||||
|
number.can_sleep(spring)
|
||||||
|
else
|
||||||
|
exhaustive(spring.type)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
step = step,
|
||||||
|
can_sleep = can_sleep,
|
||||||
|
}
|
||||||
136
modules/Spring/number.luau
Executable file
136
modules/Spring/number.luau
Executable file
|
|
@ -0,0 +1,136 @@
|
||||||
|
--!native
|
||||||
|
--!optimize 2
|
||||||
|
--!strict
|
||||||
|
|
||||||
|
export type Spring = {
|
||||||
|
type: "number",
|
||||||
|
d: number,
|
||||||
|
f: number,
|
||||||
|
g: number,
|
||||||
|
p: number,
|
||||||
|
v: number
|
||||||
|
}
|
||||||
|
|
||||||
|
local EPS = 1e-5 -- epsilon for stability checks around pathological frequency/damping values
|
||||||
|
|
||||||
|
local function create(d: number, f: number, origo: number, goal: number): Spring
|
||||||
|
return {
|
||||||
|
type = "number",
|
||||||
|
d = d,
|
||||||
|
f = f,
|
||||||
|
g = goal,
|
||||||
|
p = origo,
|
||||||
|
v = 0,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function step(spring: Spring, dt: number): number
|
||||||
|
debug.profilebegin("Vector3 Linear Spring")
|
||||||
|
|
||||||
|
local f = spring.f
|
||||||
|
local d = spring.d
|
||||||
|
local g = spring.g
|
||||||
|
local v = spring.v
|
||||||
|
local p = spring.p
|
||||||
|
|
||||||
|
if d == 1 then -- critically damped
|
||||||
|
local q = math.exp(-f*dt)
|
||||||
|
local w = dt*q
|
||||||
|
|
||||||
|
local c0 = q + w*f
|
||||||
|
local c2 = q - w*f
|
||||||
|
local c3 = w*f*f
|
||||||
|
|
||||||
|
local o = p - g
|
||||||
|
|
||||||
|
p = o*c0+v*w+g
|
||||||
|
v = v * c2-o*c3
|
||||||
|
elseif d < 1 then -- underdamped
|
||||||
|
local q = math.exp(-d*f*dt)
|
||||||
|
local c = math.sqrt(1 - d*d)
|
||||||
|
|
||||||
|
local i = math.cos(dt*f*c)
|
||||||
|
local j = math.sin(dt*f*c)
|
||||||
|
|
||||||
|
-- Damping ratios approaching 1 can cause division by very small numbers.
|
||||||
|
-- To mitigate that, group terms around z=j/c and find an approximation for z.
|
||||||
|
-- Start with the definition of z:
|
||||||
|
-- z = sin(dt*f*c)/c
|
||||||
|
-- Substitute a=dt*f:
|
||||||
|
-- z = sin(a*c)/c
|
||||||
|
-- Take the Maclaurin expansion of z with respect to c:
|
||||||
|
-- z = a - (a^3*c^2)/6 + (a^5*c^4)/120 + O(c^6)
|
||||||
|
-- z ≈ a - (a^3*c^2)/6 + (a^5*c^4)/120
|
||||||
|
-- Rewrite in Horner form:
|
||||||
|
-- z ≈ a + ((a*a)*(c*c)*(c*c)/20 - c*c)*(a*a*a)/6
|
||||||
|
|
||||||
|
local z
|
||||||
|
if c > EPS then
|
||||||
|
z = j/c
|
||||||
|
else
|
||||||
|
local a = dt*f
|
||||||
|
z = a + ((a*a)*(c*c)*(c*c)/20 - c*c)*(a*a*a)/6
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Frequencies approaching 0 present a similar problem.
|
||||||
|
-- We want an approximation for y as f approaches 0, where:
|
||||||
|
-- y = sin(dt*f*c)/(f*c)
|
||||||
|
-- Substitute b=dt*c:
|
||||||
|
-- y = sin(b*c)/b
|
||||||
|
-- Now reapply the process from z.
|
||||||
|
|
||||||
|
local y
|
||||||
|
if f*c > EPS then
|
||||||
|
y = j/(f*c)
|
||||||
|
else
|
||||||
|
local b = f*c
|
||||||
|
y = dt + ((dt*dt)*(b*b)*(b*b)/20 - b*b)*(dt*dt*dt)/6
|
||||||
|
end
|
||||||
|
|
||||||
|
local o = p - g
|
||||||
|
|
||||||
|
p = o*(i+z*d)+v*y+g
|
||||||
|
v = (v*(i-z*d)-o*(z*f))*q
|
||||||
|
|
||||||
|
else -- overdamped
|
||||||
|
local c = math.sqrt(d*d - 1)
|
||||||
|
|
||||||
|
local r1 = -f*(d + c)
|
||||||
|
local r2 = -f*(d - c)
|
||||||
|
|
||||||
|
local ec1 = math.exp(r1*dt)
|
||||||
|
local ec2 = math.exp(r2*dt)
|
||||||
|
|
||||||
|
local o = p - g
|
||||||
|
local co2 = (v - o*r1)/(2*f*c)
|
||||||
|
local co1 = ec1*(o - co2)
|
||||||
|
p = co1 + co2*ec2 + g
|
||||||
|
v = co1*r1+co2*ec2*r2
|
||||||
|
end
|
||||||
|
|
||||||
|
spring.p = p
|
||||||
|
spring.v = v
|
||||||
|
debug.profileend()
|
||||||
|
return p
|
||||||
|
end
|
||||||
|
|
||||||
|
local SLEEP_OFFSET_SQ_LIMIT = (1/3840)^2 -- square of the offset sleep limit
|
||||||
|
local SLEEP_VELOCITY_SQ_LIMIT = 1e-2^2 -- square of the velocity sleep limit
|
||||||
|
|
||||||
|
local function can_sleep(spring: Spring): boolean
|
||||||
|
if spring.v^2 > SLEEP_VELOCITY_SQ_LIMIT then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
if (spring.p - spring.g)^2 > SLEEP_OFFSET_SQ_LIMIT then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
create = create,
|
||||||
|
step = step,
|
||||||
|
can_sleep = can_sleep,
|
||||||
|
}
|
||||||
149
modules/Spring/vector2.luau
Executable file
149
modules/Spring/vector2.luau
Executable file
|
|
@ -0,0 +1,149 @@
|
||||||
|
--!native
|
||||||
|
--!optimize 2
|
||||||
|
--!strict
|
||||||
|
|
||||||
|
local Spring_Vector3 = require("./vector3")
|
||||||
|
|
||||||
|
export type Spring = {
|
||||||
|
type: "Vector2",
|
||||||
|
d: number,
|
||||||
|
f: number,
|
||||||
|
g: Vector2,
|
||||||
|
p: Vector3,
|
||||||
|
v: Vector3
|
||||||
|
}
|
||||||
|
|
||||||
|
local EPS = 1e-5 -- epsilon for stability checks around pathological frequency/damping values
|
||||||
|
|
||||||
|
local function create(d: number, f: number, origo: Vector2, goal: Vector2): Spring
|
||||||
|
return {
|
||||||
|
type = "Vector2",
|
||||||
|
d = d,
|
||||||
|
f = f,
|
||||||
|
g = goal,
|
||||||
|
p = Vector3.new(origo.X, origo.Y, 0),
|
||||||
|
v = Vector3.zero,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function step(spring: Spring, dt: number): Vector3
|
||||||
|
debug.profilebegin("Vector2 Linear Spring")
|
||||||
|
|
||||||
|
local f = spring.f
|
||||||
|
local d = spring.d
|
||||||
|
local g = spring.g
|
||||||
|
local v = spring.v
|
||||||
|
local p = spring.p
|
||||||
|
|
||||||
|
if d == 1 then -- critically damped
|
||||||
|
local q = math.exp(-f*dt)
|
||||||
|
local w = dt*q
|
||||||
|
|
||||||
|
local c0 = q + w*f
|
||||||
|
local c2 = q - w*f
|
||||||
|
local c3 = w*f*f
|
||||||
|
|
||||||
|
local ox = p.X - g.X
|
||||||
|
local oy = p.Y - g.Y
|
||||||
|
|
||||||
|
p = Vector3.new(
|
||||||
|
ox*c0+v.X*w+g.X,
|
||||||
|
oy*c0+v.Y*w+g.Y
|
||||||
|
)
|
||||||
|
v = Vector3.new(
|
||||||
|
v.X*c2-ox*c3,
|
||||||
|
v.Y*c2-oy*c3
|
||||||
|
)
|
||||||
|
elseif d < 1 then -- underdamped
|
||||||
|
local q = math.exp(-d*f*dt)
|
||||||
|
local c = math.sqrt(1 - d*d)
|
||||||
|
|
||||||
|
local i = math.cos(dt*f*c)
|
||||||
|
local j = math.sin(dt*f*c)
|
||||||
|
|
||||||
|
-- Damping ratios approaching 1 can cause division by very small numbers.
|
||||||
|
-- To mitigate that, group terms around z=j/c and find an approximation for z.
|
||||||
|
-- Start with the definition of z:
|
||||||
|
-- z = sin(dt*f*c)/c
|
||||||
|
-- Substitute a=dt*f:
|
||||||
|
-- z = sin(a*c)/c
|
||||||
|
-- Take the Maclaurin expansion of z with respect to c:
|
||||||
|
-- z = a - (a^3*c^2)/6 + (a^5*c^4)/120 + O(c^6)
|
||||||
|
-- z ≈ a - (a^3*c^2)/6 + (a^5*c^4)/120
|
||||||
|
-- Rewrite in Horner form:
|
||||||
|
-- z ≈ a + ((a*a)*(c*c)*(c*c)/20 - c*c)*(a*a*a)/6
|
||||||
|
|
||||||
|
local z
|
||||||
|
if c > EPS then
|
||||||
|
z = j/c
|
||||||
|
else
|
||||||
|
local a = dt*f
|
||||||
|
z = a + ((a*a)*(c*c)*(c*c)/20 - c*c)*(a*a*a)/6
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Frequencies approaching 0 present a similar problem.
|
||||||
|
-- We want an approximation for y as f approaches 0, where:
|
||||||
|
-- y = sin(dt*f*c)/(f*c)
|
||||||
|
-- Substitute b=dt*c:
|
||||||
|
-- y = sin(b*c)/b
|
||||||
|
-- Now reapply the process from z.
|
||||||
|
|
||||||
|
local y
|
||||||
|
if f*c > EPS then
|
||||||
|
y = j/(f*c)
|
||||||
|
else
|
||||||
|
local b = f*c
|
||||||
|
y = dt + ((dt*dt)*(b*b)*(b*b)/20 - b*b)*(dt*dt*dt)/6
|
||||||
|
end
|
||||||
|
|
||||||
|
local ox = p.X - g.X
|
||||||
|
local oy = p.Y - g.Y
|
||||||
|
|
||||||
|
p = Vector3.new(
|
||||||
|
(ox*(i + z*d) + v.X*y)*q + g.X,
|
||||||
|
(oy*(i + z*d) + v.Y*y)*q + g.Y
|
||||||
|
)
|
||||||
|
v = Vector3.new(
|
||||||
|
(v.X*(i - z*d) - ox*(z*f))*q,
|
||||||
|
(v.Y*(i - z*d) - oy*(z*f))*q
|
||||||
|
)
|
||||||
|
|
||||||
|
else -- overdamped
|
||||||
|
local c = math.sqrt(d*d - 1)
|
||||||
|
|
||||||
|
local r1 = -f*(d + c)
|
||||||
|
local r2 = -f*(d - c)
|
||||||
|
|
||||||
|
local ec1 = math.exp(r1*dt)
|
||||||
|
local ec2 = math.exp(r2*dt)
|
||||||
|
|
||||||
|
local ox = p.X - g.X
|
||||||
|
local oy = p.Y - g.Y
|
||||||
|
|
||||||
|
local co2x = (v.X - ox*r1)/(2*f*c)
|
||||||
|
local co2y = (v.Y - oy*r1)/(2*f*c)
|
||||||
|
|
||||||
|
local co1x = ec1*(ox - co2x)
|
||||||
|
local co1y = ec1*(oy - co2y)
|
||||||
|
|
||||||
|
p = Vector3.new(
|
||||||
|
co1x + co2x*ec2 + g.X,
|
||||||
|
co1y + co2y*ec2 + g.Y
|
||||||
|
)
|
||||||
|
v = Vector3.new(
|
||||||
|
co1x*r1 + co2x*ec2*r2,
|
||||||
|
co1y*r1 + co2y*ec2*r2
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
spring.p = p
|
||||||
|
spring.v = v
|
||||||
|
debug.profileend()
|
||||||
|
return p
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
create = create,
|
||||||
|
step = step,
|
||||||
|
can_sleep = Vector3_Spring.can_sleep,
|
||||||
|
}
|
||||||
133
modules/Spring/vector3.luau
Executable file
133
modules/Spring/vector3.luau
Executable file
|
|
@ -0,0 +1,133 @@
|
||||||
|
--!native
|
||||||
|
--!optimize 2
|
||||||
|
--!strict
|
||||||
|
|
||||||
|
export type Spring = {
|
||||||
|
type: "Vector3",
|
||||||
|
d: number,
|
||||||
|
f: number,
|
||||||
|
g: Vector3,
|
||||||
|
p: Vector3,
|
||||||
|
v: vector
|
||||||
|
}
|
||||||
|
|
||||||
|
local EPS = 1e-5 -- epsilon for stability checks around pathological frequency/damping values
|
||||||
|
|
||||||
|
local function create(d: number, f: number, origo: Vector3, goal: Vector3): Spring
|
||||||
|
return {
|
||||||
|
type = "Vector3",
|
||||||
|
d = d,
|
||||||
|
f = f,
|
||||||
|
g = goal,
|
||||||
|
p = origo,
|
||||||
|
v = vector.create(0, 0, 0),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function step(spring: Spring, dt: number): Vector3
|
||||||
|
local f = spring.f
|
||||||
|
local d = spring.d
|
||||||
|
local g = (spring.g :: any)::vector
|
||||||
|
local v = spring.v
|
||||||
|
local p = (spring.p :: any)::vector
|
||||||
|
|
||||||
|
if d == 1 then -- critically damped
|
||||||
|
local q = math.exp(-f*dt)
|
||||||
|
local w = dt*q
|
||||||
|
|
||||||
|
local c0 = q + w*f
|
||||||
|
local c2 = q - w*f
|
||||||
|
local c3 = w*f*f
|
||||||
|
|
||||||
|
local o = p - g
|
||||||
|
p = o * c0 + v * w + g
|
||||||
|
v = v * c2 - o * c3
|
||||||
|
elseif d < 1 then -- underdamped
|
||||||
|
local q = math.exp(-d*f*dt)
|
||||||
|
local c = math.sqrt(1 - d*d)
|
||||||
|
|
||||||
|
local i = math.cos(dt*f*c)
|
||||||
|
local j = math.sin(dt*f*c)
|
||||||
|
|
||||||
|
-- Damping ratios approaching 1 can cause division by very small numbers.
|
||||||
|
-- To mitigate that, group terms around z=j/c and find an approximation for z.
|
||||||
|
-- Start with the definition of z:
|
||||||
|
-- z = sin(dt*f*c)/c
|
||||||
|
-- Substitute a=dt*f:
|
||||||
|
-- z = sin(a*c)/c
|
||||||
|
-- Take the Maclaurin expansion of z with respect to c:
|
||||||
|
-- z = a - (a^3*c^2)/6 + (a^5*c^4)/120 + O(c^6)
|
||||||
|
-- z ≈ a - (a^3*c^2)/6 + (a^5*c^4)/120
|
||||||
|
-- Rewrite in Horner form:
|
||||||
|
-- z ≈ a + ((a*a)*(c*c)*(c*c)/20 - c*c)*(a*a*a)/6
|
||||||
|
|
||||||
|
local z
|
||||||
|
if c > EPS then
|
||||||
|
z = j/c
|
||||||
|
else
|
||||||
|
local a = dt*f
|
||||||
|
z = a + ((a*a)*(c*c)*(c*c)/20 - c*c)*(a*a*a)/6
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Frequencies approaching 0 present a similar problem.
|
||||||
|
-- We want an approximation for y as f approaches 0, where:
|
||||||
|
-- y = sin(dt*f*c)/(f*c)
|
||||||
|
-- Substitute b=dt*c:
|
||||||
|
-- y = sin(b*c)/b
|
||||||
|
-- Now reapply the process from z.
|
||||||
|
|
||||||
|
local y
|
||||||
|
if f*c > EPS then
|
||||||
|
y = j/(f*c)
|
||||||
|
else
|
||||||
|
local b = f*c
|
||||||
|
y = dt + ((dt*dt)*(b*b)*(b*b)/20 - b*b)*(dt*dt*dt)/6
|
||||||
|
end
|
||||||
|
|
||||||
|
local o = p - g
|
||||||
|
|
||||||
|
p = (o * (i + z*d) + v * y) * q + g
|
||||||
|
v = (v * (i - z*d) - o * (z*f)) * q
|
||||||
|
else -- overdamped
|
||||||
|
local c = math.sqrt(d*d - 1)
|
||||||
|
|
||||||
|
local r1 = -f*(d + c)
|
||||||
|
local r2 = -f*(d - c)
|
||||||
|
|
||||||
|
local ec1 = math.exp(r1*dt)
|
||||||
|
local ec2 = math.exp(r2*dt)
|
||||||
|
|
||||||
|
local o = p - g
|
||||||
|
|
||||||
|
local co2 = (v - o*r1)/(2*f*c)
|
||||||
|
local co1 = ec1*(o - co2)
|
||||||
|
|
||||||
|
p = co1 + co2*ec2 + g
|
||||||
|
v = co1*r1 + co2*ec2*r2
|
||||||
|
end
|
||||||
|
|
||||||
|
spring.p = (p :: any) :: Vector3
|
||||||
|
spring.v = (v :: any) :: Vector3
|
||||||
|
return (p :: any) :: Vector3
|
||||||
|
end
|
||||||
|
|
||||||
|
local SLEEP_OFFSET_SQ_LIMIT = (1/3840)^2 -- square of the offset sleep limit
|
||||||
|
local SLEEP_VELOCITY_SQ_LIMIT = 1e-2^2 -- square of the velocity sleep limit
|
||||||
|
|
||||||
|
local function can_sleep(spring: Spring): boolean
|
||||||
|
if vector.magnitude(spring.v)^2 > SLEEP_VELOCITY_SQ_LIMIT then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
if (spring.p - spring.g).Magnitude^2 > SLEEP_OFFSET_SQ_LIMIT then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
create = create,
|
||||||
|
step = step,
|
||||||
|
can_sleep = can_sleep,
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue