diff --git a/mods/artifact_base/init.lua b/mods/artifact_base/init.lua index 1844dab..b83fde1 100644 --- a/mods/artifact_base/init.lua +++ b/mods/artifact_base/init.lua @@ -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"}, diff --git a/mods/artifact_hud/init.lua b/mods/artifact_hud/init.lua index e69de29..8049304 100644 --- a/mods/artifact_hud/init.lua +++ b/mods/artifact_hud/init.lua @@ -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 +} diff --git a/mods/artifact_hud/textures/artifact_construct_test_icon.png b/mods/artifact_hud/textures/artifact_construct_test_icon.png new file mode 100644 index 0000000..9f44b88 Binary files /dev/null and b/mods/artifact_hud/textures/artifact_construct_test_icon.png differ diff --git a/mods/artifact_hud/textures/artifact_radial_cursor.png b/mods/artifact_hud/textures/artifact_radial_cursor.png new file mode 100644 index 0000000..cf731b1 Binary files /dev/null and b/mods/artifact_hud/textures/artifact_radial_cursor.png differ diff --git a/mods/artifact_hud/textures/crosshair.png b/mods/artifact_hud/textures/crosshair.png new file mode 100644 index 0000000..4b0660b Binary files /dev/null and b/mods/artifact_hud/textures/crosshair.png differ diff --git a/mods/artifact_player/init.lua b/mods/artifact_player/init.lua index 79826fc..f77b4b8 100644 --- a/mods/artifact_player/init.lua +++ b/mods/artifact_player/init.lua @@ -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 diff --git a/mods/artifact_player/radial_menu.lua b/mods/artifact_player/radial_menu.lua new file mode 100644 index 0000000..22c9674 --- /dev/null +++ b/mods/artifact_player/radial_menu.lua @@ -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