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, pitch = 0, yaw = 0 }, {__index = Player}) m.name = p:get_player_name() m.meta = p:get_meta() m.character = m.meta:get("character") or "key" m.spawn_point = m.meta:get("spawnpoint") or artifact.origin m.color = m.meta:get("color") m.inv = p:get_inventory() m.inv:set_stack("main", 1, ItemStack("input_"..m.character)) -- Generic black sky, since the whole game takes place underground. p:set_sky{ type = "basic", base_color = "#000", clouds = false } p:set_sun{visible = false} p:set_moon{visible = false} p:set_stars{visible = false} p:set_properties { visual = "mesh", mesh = "artifact_character.gltf", shaded = false } p:hud_set_flags { healthbar = false, breathbar = false, hotbar = artifact.debug, minimap = false, basic_debug = artifact.debug, crosshair = false, -- It gets set to true once we can play. wielditem = false, -- Ditto. chat = false, -- We provide our own implementation of the chat HUD. } -- The following exists to make sure that whatever physics -- settings the server may have set are transparently ignored. local defaults = { speed_walk = 4, speed_crouch = 1.35, speed_fast = 20, speed_climb = 3, speed_jump = 6.5, gravity = 9.81, liquid_fluidity = 1, liquid_fluidity_smooth = 0.5, liquid_sink = 10, acceleration_default = 3, acceleration_air = 2, acceleration_fast = 10, } local override = { speed = 1, sneak = true, sneak_glitch = false, new_move = true, } for key, def_value in pairs(defaults) do local setting_name = "movement_"..key local current = tonumber(minetest.settings:get(setting_name)) or def_value override[key] = def_value /current end p:set_physics_override(override) -- No unreasonable FOV settings here. p:set_fov(72) p:set_fov(0, true, 0) -- Tell raycast collisions that we're a player. p:set_armor_groups(extend(p:get_armor_groups(), {player = 1})) m.hud = {} m.poi = {} m.chat = {} if not artifact.debug then p:set_inventory_formspec "" end if m.character == "vix" then artifact.apply_vix(m) else artifact.apply_key(m) end -- Let us build in debug mode, but ensure we always wield the hand item otherwise. m:set_hotbar_size(artifact.debug and 8 or 1) m.ctl = p:get_player_control() return m end, tick = function(m) 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() local vel = p:get_velocity() local speed = vel:length() m.pos = pos m.pos.y = m.pos.y +m.eye_height local state = artifact.story.get_state() -- Sleep if we are not yet ready for the player to do things. if not artifact.debug and state <= artifact.story.states.init then return end -- MARK: Pointing callbacks local pointed_found = nil m.pointed_node = nil for x in minetest.raycast(m.pos, m.pos +(dir *5)) do -- We should ignore all objects when placing a grabbed node. if x and x.type == "object" and not m._grabbed_item then local e = x.ref:get_luaentity() -- Ignore players. if e then local names_match = m.pointed_obj and (m.pointed_obj._name or m.pointed_obj.name) == (e._name or e.name) if m.pointed_obj and not names_match then if m.pointed_obj.on_unhover then m.pointed_obj:on_unhover(m) end if m.pointed_obj.on_interact and m.interaction_marker then m.object:hud_remove(m.interaction_marker) m.interaction_marker = nil m.interaction_start = nil end end if e.on_interact and not e._no_interact and (not names_match or names_match and not m.interaction_marker) and (not e._can_interact or e:_can_interact(m)) then if m.interaction_marker then m.object:hud_remove(m.interaction_marker) end local dst = e.object:get_pos() if e._interact_marker_offset then dst = dst +e:_interact_marker_offset() end m.interaction_marker = m.object:hud_add { type = "image_waypoint", world_pos = dst, scale = {x=3, y=3}, text = "artifact_rmb.png" } end if (m.pointed_obj and not names_match and e.on_hover) or not m.pointed_obj then if e.on_hover then e:on_hover(m) end pointed_found = true m.pointed_obj = e break elseif m.pointed_obj and names_match then pointed_found = true break end end elseif x and x.type == "node" then m.pointed_node = x if m.pointed_obj then if m.pointed_obj.on_unhover then m.pointed_obj:on_unhover(m) end if m.pointed_obj.on_interact and m.interaction_marker then m.object:hud_remove(m.interaction_marker) m.interaction_marker = nil m.interaction_start = nil end m.pointed_obj = nil end break end end if not pointed_found and m.pointed_obj then if m.pointed_obj.on_unhover then m.pointed_obj:on_unhover(m) end if m.pointed_obj.on_interact and m.interaction_marker then m.object:hud_remove(m.interaction_marker) m.interaction_marker = nil m.interaction_start = nil end m.pointed_obj = nil end local ctl = m.object:get_player_control() -- MARK: Animations local moving = (ctl.up or ctl.down or ctl.left or ctl.right) and speed > 0.1 if moving then m.moving = true if ctl.aux1 and ctl.up then if p:get_animation().y ~= 2 then p:set_animation({x=1, y=2}, 1.5, 0.2, true) end p:set_physics_override{ speed = 1.5 } p:set_fov(1.1, true, 0.2) else if p:get_animation().y ~= 1 then p:set_animation({x=0, y=1}, 1.5, 0.2, true) end p:set_physics_override{ speed = 1 } p:set_fov(0, true, 0.2) end else -- We can't call this unconditionally because during the first couple -- globalsteps, doing so will heavily distort the player's FOV for -- some reason. Since `m.moving` is never set to true until the player -- starts sprinting, we can sidestep this issue fairly trivially. if m.moving then p:set_physics_override{ speed = 1 } p:set_fov(0, true, 0.2) end m.moving = false if p:get_animation().y ~= 0 then p:set_animation({x=0, y=0}) end end if not m.rot then m.rot = 0 end if moving then local fac = 0 if ctl.left then fac = 30 elseif ctl.right then fac = -30 end m.rot = yaw +math.rad(fac) elseif math.abs(yaw -m.rot) > math.rad(40) then m.rot = m.rot +(yaw -(m.yaw or 0)) end m.rot = m.rot %(math.pi *2) p:set_bone_override("Head", { rotation = {vec = vector.new(math.min(math.max(pitch, math.rad(-60)), math.rad(60)),-(yaw -m.rot),0), interpolation = 0.1, absolute = true} }) p:set_bone_override("root", { rotation = {vec = vector.new(0,yaw -m.rot,0), interpolation = 0.1, absolute = true} }) -- Handle grabbed devices. This trumps other input handling like the radial menu and on_interact. if m._grabbed_item then m._grabbed_item:move_to(m.pos +(dir *2)) if ctl.place and m.pointed_node then m._grabbed_item:move_to(m.pointed_node.above) m._grabbed_item = nil -- This should be set dynamically by whatever function put us into the grabbing -- state, and accordingly should only be valid for the duration of that state. if m._on_ungrab then m._on_ungrab() m._on_ungrab = nil end end -- This code is duplicated from the bottom... but since the -- only cleaner alternative is goto, I decided to support PUC Lua. if m.next_regen and time -m.next_regen >= 0 then m.object:set_hp(m.object:get_hp() +1) end m.ctl = ctl m.yaw = yaw m.pitch = pitch m.dir = dir return end -- MARK: Progressive interaction if ctl.place and m.ctl.place and m.pointed_obj and m.pointed_obj.on_interact and not m.pointed_obj._no_interact and (not m.pointed_obj._can_interact or m.pointed_obj:_can_interact(m)) then if not m.interaction_start then m.interaction_start = time else local duration = (m.pointed_obj._interact_time or 1) *1000000 local progress = (time -m.interaction_start) /duration if progress > 1.1 then m.pointed_obj:on_interact(m) m.interaction_start = nil m.object:hud_remove(m.interaction_marker) m.interaction_marker = nil elseif progress > 1 then m.object:hud_change(m.interaction_marker, "text", "artifact_rmb_100.png") elseif progress > 0.75 then m.object:hud_change(m.interaction_marker, "text", "artifact_rmb_75.png") elseif progress > 0.5 then m.object:hud_change(m.interaction_marker, "text", "artifact_rmb_50.png") elseif progress > 0.25 then m.object:hud_change(m.interaction_marker, "text", "artifact_rmb_25.png") end end elseif not ctl.place and m.interaction_start and (not m.pointed_obj or not m.pointed_obj._no_interact or m.pointed_obj._can_interact and m.pointed_obj:_can_interact(m)) then m.interaction_start = nil if m.interaction_marker then m.object:hud_change(m.interaction_marker, "text", "artifact_rmb.png") end end local wi = p:get_wielded_item() m.wielded_item = wi -- MARK: Radial menu handling -- This should only work once we have Vix, since we can't use it without her. if state >= artifact.story.states.main and ctl.place and not m.ctl.place and wi:get_name():find "artifact:input" and (not m.pointed_obj or not m.pointed_obj.on_interact or m.pointed_obj._no_interact) then artifact.show_radial_menu(m, { name = "construct", "test", "test2", "test3", "test4", "test5" }) elseif m._menu and not (ctl.place and wi:get_name():find "artifact:input") or (m._menu and m.pointed_obj and m.pointed_obj.on_interact and not m.pointed_obj._no_interact) then local sel = m._menu[m._menu.selected] if sel then local choice = sel.item if choice == "test" then artifact.summon_device(m, "block") end end 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 -- MARK: Health regen if m.next_regen and time -m.next_regen >= 0 then m.object:set_hp(m.object:get_hp() +1) end m.ctl = ctl m.yaw = yaw m.pitch = pitch m.dir = dir end, set_character = function(m, to) m.character = to m.meta:set_string("character", to) if to == "vix" then if m.color_hud then m.object:hud_remove(m.color_hud) m.color_hud = nil end else if m.color and not m.color_hud then m.color_hud = m.object:hud_add { type = "image", position = {x=0.5,y=1}, offset = {x=0,y=0}, alignment = {x=0,y=-1}, scale = {x=500,y=5}, text = "[fill:1x1:0,0:"..artifact.colors[m.color], } end end end, set_color = function(m, color) if artifact.debug or artifact.story.get_state() > artifact.story.states.pre_vix and m.character == "key" then m.color = color m.meta:set_string("color", color or "") if m.color_hud then m.object:hud_remove(m.color_hud) end -- If we cleared the color by passing nil, there's no need to animate or re-add the HUD. if not color then return end m.color_hud = m.object:hud_add { type = "image", position = {x=0.5,y=1}, offset = {x=0,y=0}, alignment = {x=0,y=-1}, scale = {x=500,y=5}, text = "[fill:1x1:0,0:"..artifact.colors[m.color], } local el = artifact.hud_add(m, { type = "image", pos = {x=0.5,y=0.5}, scale = {x=10000,y=10000}, opacity = 0, image = "[fill:1x1:"..artifact.colors[m.color] }) el:animate { opacity = { value = 25, duration = 0.3 } } minetest.after(0.3, function() el:animate { opacity = { value = 0, duration = 0.3 } } minetest.after(0.3, function() el:remove(m) end) end) end end, add_health_bar = function(m) m.healthbar = m.object:hud_add { type = "statbar", position = {x=0.5,y=1}, offset = {x=-27 *5,y=artifact.debug and -96 or -42}, scale = {x=4,y=4}, alignment = {x=-1, y=-1}, size = {x=27,y=27}, text = m.character == "vix" and "artifact_heart_vix.png" or "artifact_heart.png", text2 = "artifact_heart_bg.png", number = 20, item = 20, } end, set_hotbar_size = function(m, slots) local p = m.object p:hud_set_hotbar_itemcount(slots) local list = "" for i = 0, slots do list = list..":"..(21*i)..",0=artifact_hotbar_bg.png" end p:hud_set_hotbar_image("[combine:"..(21 *slots +1).."x22"..list) p:hud_set_hotbar_selected_image("artifact_hotbar_selected_bg.png") end, set_spawnpoint = function(m, pos) m.spawn_point = pos m.meta:set_string("spawnpoint", pos:to_string()) end }, { __call = function(_, ...) return Player.new(...) end }) -- Override respawning, so we can save progress. minetest.register_on_respawnplayer(function(p) local m = artifact.players[p:get_player_name()] if m.spawn_point then p:set_pos(m.spawn_point) return true end end) -- Mirror the player's HP in our custom HUD. -- (We need a custom HUD so that we can change its appearance dynamically.) minetest.register_on_player_hpchange(function(p, delta) local m = artifact.players[p:get_player_name()] local hp = p:get_hp() +delta if m.healthbar then p:hud_change(m.healthbar, "number", hp) end if hp < 20 then m.next_regen = minetest.get_us_time() +5000000 else m.next_regen = nil end end) local _hand = minetest.registered_items[""] function artifact.register_input(name) artifact.register_node("input_"..name, { inventory_image = "artifact_rmb_100.png", description = "", paramtype = "light", drawtype = "mesh", mesh = name == "key" and "artifact_hand_key.gltf" or "artifact_hand.gltf", tiles = name == "key" and {"artifact_blackrod.png"} or {"artifact_"..name..".png", "artifact_blackrod.png"}, use_texture_alpha = "opaque", visual_scale = 1, wield_scale = vector.new(2,2,2), node_placement_prediction = "", on_construct = function(pos) minetest.remove_node(pos) end, drop = "", range = 0, pointabilities = { nodes = { ["group:everything"] = false }, objects = { ["group:immortal"] = false, ["group:fleshy"] = false } }, on_drop = function(s, p, pos) local m = artifact.players[p:get_player_name()] if not m._swapping_character and (artifact.debug or artifact.story.get_state() > artifact.story.states.pre_vix) then artifact.swap_character(m) -- If Key was pointing at something Vix shouldn't be able to interact with, -- but Vix is also pointing at it, then remove the interaction marker since -- it would be misleading. if m.pointed_obj and m.interaction_marker and (not m.pointed_obj._can_interact or m.pointed_obj:_can_interact(m)) then m.object:hud_remove(m.interaction_marker) m.interaction_marker = nil m.interaction_start = nil end end return s end, on_use = function(s, p) local m = artifact.players[p:get_player_name()] if m._grabbed_item then return end if m.pointed_obj and m.pointed_obj._grabbable then artifact.grab_device(m, m.pointed_obj) return end if m.character == "vix" then artifact.do_shoot(m) else artifact.do_whack(m) end end }) end artifact.register_input "key" artifact.register_input "vix" -- Apparently the hand range is applied very briefly when switching items. if not artifact.debug then minetest.override_item("", {range = 0}) end minetest.register_globalstep(function() for _, m in pairs(artifact.players) do m:tick() end end) minetest.register_on_joinplayer(function(p) artifact.players[p:get_player_name()] = Player(p) if artifact.debug then -- Make sure we don't have to `/grantme` a million times while testing. minetest.registered_chatcommands.grantme.func(p:get_player_name(), "all") end end) minetest.register_on_leaveplayer(function(p) artifact.players[p:get_player_name()] = nil end)