Interpolated HUD and radial menu UI.
This commit is contained in:
parent
5211cc20f2
commit
6439f11c2f
7 changed files with 456 additions and 30 deletions
|
|
@ -22,6 +22,30 @@ end
|
|||
|
||||
say = minetest.chat_send_all
|
||||
|
||||
function extend(dst, src)
|
||||
for k, v in pairs(src) do
|
||||
dst[k] = v
|
||||
end
|
||||
return dst
|
||||
end
|
||||
|
||||
-- Some kind of promise API. (Forget about async-await, though.)
|
||||
function Promise(fn)
|
||||
local p = {resolved = false}
|
||||
p.resolve = function(...)
|
||||
if p.resolved then return end
|
||||
p.resolved = true
|
||||
if p.after then p.after(...) end
|
||||
end
|
||||
fn(p.resolve)
|
||||
return {
|
||||
after = function(fn)
|
||||
p.after = fn
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
minetest.register_lbm{
|
||||
name = ":artifact:on_load",
|
||||
nodenames = {"group:call_on_load"},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,274 @@
|
|||
|
||||
local ns = artifact
|
||||
|
||||
ns.hud_types = {}
|
||||
ns.Element = {
|
||||
animate = function(e, target)
|
||||
if not e.targets then e.targets = {} end
|
||||
local time = minetest.get_us_time()
|
||||
for k, v in pairs(target) do
|
||||
e.targets[k] = {
|
||||
ref = {
|
||||
time = time,
|
||||
value = e[k]
|
||||
},
|
||||
target = {
|
||||
time = time +(v.duration or 1) *1000000,
|
||||
value = v.value
|
||||
},
|
||||
ease_fn = v.ease_fn
|
||||
}
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
local function bezier_ease(t, x1, y1, x2, y2)
|
||||
if t <= 0 then return 0 end
|
||||
if t >= 1 then return 1 end
|
||||
|
||||
local low = 0
|
||||
local high = 1
|
||||
local epsilon = 1e-6
|
||||
local iterations = 0
|
||||
while (high - low > epsilon) and (iterations < 100) do
|
||||
local mid = (low + high) / 2
|
||||
local x = 3 * mid * (1 - mid) ^ 2 * x1 + 3 * mid ^ 2 * (1 - mid) * x2 + mid ^ 3
|
||||
if x < t then
|
||||
low = mid
|
||||
else
|
||||
high = mid
|
||||
end
|
||||
iterations = iterations + 1
|
||||
end
|
||||
local u = (low + high) / 2
|
||||
|
||||
local y = 3 * u * (1 - u) ^ 2 * y1 + 3 * u ^ 2 * (1 - u) * y2 + u ^ 3
|
||||
return y
|
||||
end
|
||||
|
||||
local function interpolate(ref, target, t, x1, y1, x2, y2)
|
||||
local eased_t = bezier_ease(t, x1 or 0, y1 or 0, x2 or 1, y2 or 1)
|
||||
return ref + (target - ref) * eased_t
|
||||
end
|
||||
|
||||
|
||||
function ns.register_hud_type(def)
|
||||
ns.hud_types[def.name] = setmetatable(def, {__index = ns.Element})
|
||||
end
|
||||
|
||||
function ns.validate_type(elem, type)
|
||||
if not ns.hud_types[elem.type] then
|
||||
warn("Unknown HUD type `"..type.."` for element `"..elem.name.."`; ignoring.")
|
||||
return false
|
||||
end
|
||||
if ns.hud_types[type].required_fields then
|
||||
for _, field in ipairs(ns.hud_types[type].required_fields) do
|
||||
if elem[field] == nil then return false end
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function ns.hud_add(m, def)
|
||||
if not ns.validate_type(def, def.type) then
|
||||
return false
|
||||
end
|
||||
local type = ns.hud_types[def.type]
|
||||
if type.defaults then
|
||||
def = extend(table.copy(type.defaults), def)
|
||||
end
|
||||
local el
|
||||
if m.hud[def.name] then
|
||||
el = m.hud[def.name]
|
||||
-- Simply write all modified fields to the existing element.
|
||||
extend(el, def)
|
||||
else
|
||||
el = setmetatable(def, {__index = type})
|
||||
m.hud[def.name] = el
|
||||
el:add(m)
|
||||
end
|
||||
return el
|
||||
end
|
||||
|
||||
function ns.update_poi(m)
|
||||
for _, x in pairs(m.poi) do
|
||||
x:remove(m)
|
||||
end
|
||||
m.poi = {}
|
||||
for _, x in ipairs(minetest.find_nodes_in_area(m.pos:offset(-100, -100, -100), m.pos:offset(100,100,100), "group:poi")) do
|
||||
m.poi[#m.poi +1] = ns.hud_add(m, {
|
||||
name = "poi:"..x:to_string(),
|
||||
type = "poi",
|
||||
world_pos = x,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
local default_ease_fn = {0,0,1,1}
|
||||
minetest.register_globalstep(function(dtime)
|
||||
local time = minetest.get_us_time()
|
||||
for _, m in pairs(artifact.players) do
|
||||
for k, el in pairs(m.hud) do
|
||||
if el.remove_after then
|
||||
el.remove_after = el.remove_after -dtime
|
||||
if el.remove_after < 0 then
|
||||
el:remove(m)
|
||||
m.hud[k] = nil
|
||||
end
|
||||
end
|
||||
|
||||
if el.targets and next(el.targets) then
|
||||
local changes = {}
|
||||
for key, target in pairs(el.targets) do
|
||||
local fac = (time -target.ref.time) /(target.target.time -target.ref.time)
|
||||
local ease_fn = target.ease_fn or default_ease_fn
|
||||
local value
|
||||
if el.field_types[key] == "vec2" then
|
||||
value = {
|
||||
x = interpolate(target.ref.value.x, target.target.value.x, fac, ease_fn[1], ease_fn[2], ease_fn[3], ease_fn[4]),
|
||||
y = interpolate(target.ref.value.y, target.target.value.y, fac, ease_fn[1], ease_fn[2], ease_fn[3], ease_fn[4])
|
||||
}
|
||||
if fac >= 1 then
|
||||
el.targets[key] = nil
|
||||
end
|
||||
elseif el.field_types[key] == "color" then
|
||||
value = {
|
||||
r = interpolate(target.ref.value.r, target.target.value.r, fac, ease_fn[1], ease_fn[2], ease_fn[3], ease_fn[4]),
|
||||
g = interpolate(target.ref.value.g, target.target.value.g, fac, ease_fn[1], ease_fn[2], ease_fn[3], ease_fn[4]),
|
||||
b = interpolate(target.ref.value.b, target.target.value.b, fac, ease_fn[1], ease_fn[2], ease_fn[3], ease_fn[4]),
|
||||
a = interpolate(target.ref.value.a, target.target.value.a, fac, ease_fn[1], ease_fn[2], ease_fn[3], ease_fn[4]),
|
||||
}
|
||||
if value.r == target.target.value.r and value.g == target.target.value.g and value.b == target.target.value.b and value.a == target.target.value.a then
|
||||
el.targets[key] = nil
|
||||
end
|
||||
else
|
||||
value = interpolate(target.ref.value, target.target.value, fac, ease_fn[1], ease_fn[2], ease_fn[3], ease_fn[4])
|
||||
if value == target.target.value then
|
||||
el.targets[key] = nil
|
||||
end
|
||||
end
|
||||
el[key] = value
|
||||
-- We could just set this to true, but since we already
|
||||
-- have a new table, we might as well use it.
|
||||
changes[key] = value
|
||||
end
|
||||
el:update(m, changes)
|
||||
end
|
||||
end
|
||||
|
||||
for k, el in ipairs(m.poi) do
|
||||
if m.dir:distance(m.pos:direction(el.world_pos)) < 0.05 then
|
||||
el.focused = true
|
||||
el:animate {
|
||||
scale = {
|
||||
value = {x=2,y=2},
|
||||
duration = 0.2,
|
||||
}
|
||||
}
|
||||
elseif el.focused then
|
||||
el.focused = false
|
||||
el:animate {
|
||||
scale = {
|
||||
value = {x=1,y=1},
|
||||
duration = 0.2,
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
function ns.color_to_number(color)
|
||||
return tonumber(string.format("0x%.2x%.2x%.2x%.2x", color.r, color.g, color.b, color.a))
|
||||
end
|
||||
|
||||
ns.register_hud_type {
|
||||
name = "text",
|
||||
required_fields = {"pos", "text"},
|
||||
field_types = {
|
||||
offset = "vec2",
|
||||
pos = "vec2",
|
||||
color = "color"
|
||||
},
|
||||
defaults = {
|
||||
dir = 0,
|
||||
align = {x=0, y=0},
|
||||
offset = {x=0, y=0},
|
||||
color = {r = 0xff, g = 0xff, b = 0xff, a = 0xff}
|
||||
},
|
||||
add = function(e, m)
|
||||
e._id = m.object:hud_add {
|
||||
type = "text",
|
||||
position = e.pos,
|
||||
direction = e.dir,
|
||||
alignment = e.align,
|
||||
offset = e.offset,
|
||||
scale = {x=100, y=100},
|
||||
text = e.text,
|
||||
number = ns.color_to_number(e.color)
|
||||
}
|
||||
end,
|
||||
update = function(e, m, changes)
|
||||
for k, v in pairs(changes) do
|
||||
if k == "color" then
|
||||
k = "number"
|
||||
v = ns.color_to_number(v)
|
||||
elseif k == "dir" then
|
||||
k = "direction"
|
||||
elseif k == "align" then
|
||||
k = "alignment"
|
||||
end
|
||||
m.object:hud_change(e._id, k, v)
|
||||
end
|
||||
end,
|
||||
remove = function(e, m)
|
||||
m.object:hud_remove(e._id)
|
||||
e._id = nil
|
||||
end
|
||||
}
|
||||
|
||||
ns.register_hud_type {
|
||||
name = "image",
|
||||
required_fields = {"pos", "image"},
|
||||
field_types = {
|
||||
offset = "vec2",
|
||||
scale = "vec2",
|
||||
pos = "vec2"
|
||||
},
|
||||
defaults = {
|
||||
dir = 0,
|
||||
align = {x=0, y=0},
|
||||
offset = {x=0, y=0},
|
||||
scale = {x=1, y=1},
|
||||
opacity = 256
|
||||
},
|
||||
add = function(e, m)
|
||||
e._id = m.object:hud_add {
|
||||
type = "image",
|
||||
position = e.pos,
|
||||
direction = e.dir,
|
||||
alignment = e.align,
|
||||
offset = e.offset,
|
||||
scale = e.scale,
|
||||
text = e.image..string.format("^[opacity:%i", e.opacity)
|
||||
}
|
||||
end,
|
||||
update = function(e, m, changes)
|
||||
for k, v in pairs(changes) do
|
||||
if k == "align" then
|
||||
k = "alignment"
|
||||
elseif k == "pos" then
|
||||
k = "position"
|
||||
elseif k == "opacity" then
|
||||
k = "text"
|
||||
v = e.image..string.format("^[opacity:%i", e.opacity)
|
||||
end
|
||||
m.object:hud_change(e._id, k, v)
|
||||
end
|
||||
end,
|
||||
remove = function(e, m)
|
||||
m.hud[e.name] = nil
|
||||
m.object:hud_remove(e._id)
|
||||
e._id = nil
|
||||
end
|
||||
}
|
||||
BIN
mods/artifact_hud/textures/artifact_construct_test_icon.png
Normal file
BIN
mods/artifact_hud/textures/artifact_construct_test_icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
mods/artifact_hud/textures/artifact_radial_cursor.png
Normal file
BIN
mods/artifact_hud/textures/artifact_radial_cursor.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 162 B |
BIN
mods/artifact_hud/textures/crosshair.png
Normal file
BIN
mods/artifact_hud/textures/crosshair.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 118 B |
|
|
@ -3,10 +3,14 @@ local ns = artifact
|
|||
ns.players = {}
|
||||
local db = minetest.get_mod_storage()
|
||||
|
||||
include "radial_menu.lua"
|
||||
|
||||
Player = setmetatable({
|
||||
new = function(p)
|
||||
local m = setmetatable({
|
||||
object = p
|
||||
object = p,
|
||||
pitch = 0,
|
||||
yaw = 0
|
||||
}, {__index = Player})
|
||||
|
||||
m.name = p:get_player_name()
|
||||
|
|
@ -46,6 +50,8 @@ Player = setmetatable({
|
|||
basic_debug = false
|
||||
}
|
||||
|
||||
m.hud = {}
|
||||
m.poi = {}
|
||||
m:create_hud()
|
||||
|
||||
m:set_hotbar_size(8)
|
||||
|
|
@ -58,6 +64,8 @@ Player = setmetatable({
|
|||
local time = minetest.get_us_time()
|
||||
local p = m.object
|
||||
local pos = p:get_pos()
|
||||
local yaw = p:get_look_horizontal()
|
||||
local pitch = p:get_look_vertical()
|
||||
local dir = p:get_look_dir()
|
||||
m.pos = pos
|
||||
m.pos.y = m.pos.y +m.eye_height
|
||||
|
|
@ -149,9 +157,81 @@ Player = setmetatable({
|
|||
m.interacting_with = nil
|
||||
m.interaction_start = nil
|
||||
end
|
||||
|
||||
local wi = p:get_wielded_item()
|
||||
|
||||
m.wielded_item = wi
|
||||
|
||||
if ctl.place and not m.ctl.place and wi:get_name() == "artifact:input" then
|
||||
artifact.show_radial_menu(m, {
|
||||
name = "construct",
|
||||
"test",
|
||||
"test2",
|
||||
"test3",
|
||||
"test4",
|
||||
"test5"
|
||||
})
|
||||
elseif m._menu and not (ctl.place and wi:get_name() == "artifact:input") then
|
||||
artifact.dismiss_radial_menu(m, "construct")
|
||||
elseif m._menu then
|
||||
local dx = m.yaw -yaw
|
||||
local dy = m.pitch -pitch
|
||||
if dx ~= 0 and dy ~= 0 then
|
||||
m._menu.pos.x = m._menu.pos.x +dx *200
|
||||
m._menu.pos.y = m._menu.pos.y -dy *200
|
||||
local r = m._menu.pos:distance(vector.zero())
|
||||
if r > 50 then
|
||||
r = 50
|
||||
m._menu.pos = m._menu.pos:normalize() *50
|
||||
end
|
||||
p:hud_change(m._menu.cursor._id, "offset", m._menu.pos)
|
||||
if r > 20 then
|
||||
local angle = minetest.dir_to_yaw(vector.new(m._menu.pos.x, 0, m._menu.pos.y):normalize())
|
||||
local idx = math.floor((-angle +math.pi +(m._menu.step /2)) %(math.pi *2) /m._menu.step) +1
|
||||
if m._menu.selected and m._menu.selected ~= idx then
|
||||
m._menu[m._menu.selected]:animate{
|
||||
scale = {
|
||||
value = {x=0.7, y=0.7},
|
||||
duration = 0.2
|
||||
},
|
||||
opacity = {
|
||||
value = 128,
|
||||
duration = 0.2
|
||||
}
|
||||
}
|
||||
end
|
||||
if m._menu.selected ~= idx and m._menu[idx] then
|
||||
m._menu.selected = idx
|
||||
m._menu[m._menu.selected]:animate{
|
||||
scale = {
|
||||
value = {x=1, y=1},
|
||||
duration = 0.2
|
||||
},
|
||||
opacity = {
|
||||
value = 256,
|
||||
duration = 0.2
|
||||
}
|
||||
}
|
||||
end
|
||||
elseif m._menu.selected then
|
||||
m._menu[m._menu.selected]:animate{
|
||||
scale = {
|
||||
value = {x=0.7, y=0.7},
|
||||
duration = 0.2
|
||||
},
|
||||
opacity = {
|
||||
value = 128,
|
||||
duration = 0.2
|
||||
}
|
||||
}
|
||||
m._menu.selected = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
m.ctl = ctl
|
||||
|
||||
m.yaw = yaw
|
||||
m.pitch = pitch
|
||||
end,
|
||||
set_character = function(m, to)
|
||||
m.character = to
|
||||
|
|
@ -162,34 +242,34 @@ Player = setmetatable({
|
|||
-- If called post-init, make sure we delete the previous HUD.
|
||||
-- This is useful when we want to recreate the HUD in response
|
||||
-- to an event, like freeing Vix.
|
||||
if m.hud then
|
||||
for _, x in pairs(m.hud) do
|
||||
if type(x) == "table" then
|
||||
for _, y in pairs(x) do
|
||||
m.object:hud_remove(y)
|
||||
end
|
||||
else
|
||||
m.object:hud_remove(x)
|
||||
end
|
||||
end
|
||||
end
|
||||
m.hud = {
|
||||
key_health = m.object:hud_add {
|
||||
type = "statbar",
|
||||
position = {x=0.5,y=1},
|
||||
offset = {x=-27 *5,y=artifact.debug and -96 or -30},
|
||||
scale = {x=4,y=4},
|
||||
alignment = {x=-1, y=-1},
|
||||
size = {x=27,y=27},
|
||||
text = "artifact_heart_vix.png",
|
||||
text2 = "artifact_heart_bg.png",
|
||||
number = 20
|
||||
}
|
||||
}
|
||||
|
||||
if artifact.debug or artifact.story.states[artifact.story.get_state()] >= artifact.story.states.main then
|
||||
|
||||
end
|
||||
-- if m.hud then
|
||||
-- for _, x in pairs(m.hud) do
|
||||
-- if type(x) == "table" then
|
||||
-- for _, y in pairs(x) do
|
||||
-- m.object:hud_remove(y)
|
||||
-- end
|
||||
-- else
|
||||
-- m.object:hud_remove(x)
|
||||
-- end
|
||||
-- end
|
||||
-- end
|
||||
-- m.hud = {
|
||||
-- key_health = m.object:hud_add {
|
||||
-- type = "statbar",
|
||||
-- position = {x=0.5,y=1},
|
||||
-- offset = {x=-27 *5,y=artifact.debug and -96 or -30},
|
||||
-- scale = {x=4,y=4},
|
||||
-- alignment = {x=-1, y=-1},
|
||||
-- size = {x=27,y=27},
|
||||
-- text = "artifact_heart_vix.png",
|
||||
-- text2 = "artifact_heart_bg.png",
|
||||
-- number = 20
|
||||
-- }
|
||||
-- }
|
||||
--
|
||||
-- if artifact.debug or artifact.story.states[artifact.story.get_state()] >= artifact.story.states.main then
|
||||
--
|
||||
-- end
|
||||
end,
|
||||
set_hotbar_size = function(m, slots)
|
||||
local p = m.object
|
||||
|
|
|
|||
48
mods/artifact_player/radial_menu.lua
Normal file
48
mods/artifact_player/radial_menu.lua
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
local ns = artifact
|
||||
|
||||
function ns.show_radial_menu(m, menu)
|
||||
m._menu = {
|
||||
name = menu.name,
|
||||
count = #menu,
|
||||
step = math.pi *2 /#menu,
|
||||
pos = vector.zero(),
|
||||
cursor = artifact.hud_add(m, {
|
||||
type = "image",
|
||||
pos = {x=0.5, y=0.5},
|
||||
offset = {x=0, y=0},
|
||||
image = "artifact_radial_cursor.png"
|
||||
})
|
||||
}
|
||||
for i, x in ipairs(menu) do
|
||||
local size = 150
|
||||
local angle = m._menu.step *(i -1) -math.pi
|
||||
local el = artifact.hud_add(m, {
|
||||
name = menu.name.."_"..i,
|
||||
type = "image",
|
||||
pos = {x=0.5,y=0.5},
|
||||
scale = {x=0.1,y=0.1},
|
||||
offset = {x=math.sin(angle) *size,y=math.cos(angle) *size},
|
||||
image = "artifact_construct_test_icon.png",
|
||||
opacity = 128
|
||||
})
|
||||
el:animate {
|
||||
scale = {
|
||||
value = {x=0.7, y=0.7},
|
||||
duration = 0.1
|
||||
}
|
||||
}
|
||||
m._menu[#m._menu +1] = el
|
||||
end
|
||||
m.object:hud_set_flags{crosshair = false}
|
||||
end
|
||||
|
||||
function ns.dismiss_radial_menu(m, name)
|
||||
-- This is in case we only want to close a specific menu while leaving others intact.
|
||||
if name and m._menu.name ~= name then return end
|
||||
for _, x in ipairs(m._menu) do
|
||||
x:remove(m)
|
||||
end
|
||||
m._menu.cursor:remove(m)
|
||||
m._menu = nil
|
||||
m.object:hud_set_flags{crosshair = true}
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue