artifact.story = { states = enum { -- We use an enum for this so that we can use relational operators to determine if the current state is before or after a target state. "loading", -- Mapgen is not yet complete. "init", -- For the opening cutscene. "pre_vix", -- The player doesn't have Vix yet. "main", -- The main game state. Progress is managed by checkpoints here. "end", -- The game is over. }, poi = { initial_cutscene = { start_pos = vector.new(0, 0, 0), end_pos = vector.new(0,0,0) } } } local ns = artifact.story local db = minetest.get_mod_storage() local state = db:get_int("state") -- Defaults to zero, i.e. "loading". artifact.origin = vector.from_string(db:get("origin") or "(0,0,0)") if artifact.debug then state = ns.states.main end function ns.enter_init_state() ns.play_intro_cutscene() end local vix_scene minetest.register_entity(":artifact:vix_scene", { initial_properties = { visual = "mesh", mesh = "artifact_scene_vix.gltf", textures = {"artifact_vix.png"} }, on_activate = function(e) if state > ns.states.pre_vix then e.object:remove() end e.object:set_armor_groups{immortal = 1} e.object:set_animation({x=0,y=2}, 0.5, 0.1, true) vix_scene = e end, on_deactivate = function(e) vix_scene = nil end, on_step = function(e) if e._can_check then for _, m in pairs(artifact.players) do if m.pos:distance(e.object:get_pos()) < 15 and m.dir:distance(m.pos:direction(e.object:get_pos())) < 0.2 then artifact.push_chat_message("Hmm...", "Key", "artifact_key_splash_low.png") e._can_check = false end end end end }) function ns.enter_pre_vix_state() for _, m in pairs(artifact.players) do m:add_health_bar() m.object:hud_set_flags { crosshair = true, wielditem = true, } m.object:set_pos(artifact.origin:offset(0, -73.5, -4)) m:set_spawnpoint(artifact.origin:offset(0, -73.5, -4)) end minetest.add_entity(artifact.origin:offset(-16.5, -72.5, -17), "artifact:vix_scene") local help = minetest.after(15, function() for _, m in pairs(artifact.players) do artifact.show_help_message(m, "Certain nodes can be broken by punching them with the blackrod.", "info") end end) db:set_string("checkpoint:pre_vix", "begin") artifact.on_whacked = function(type, target) local checkpoint = db:get("checkpoint:pre_vix") if checkpoint == "begin" then -- We're still in the start closet. if type == "object" then help:cancel() vix_scene._can_check = true end db:set_string("checkpoint:pre_vix", "in_room") elseif checkpoint == "in_room" then -- We're actually in the room. if target.node_under.name:find "forcefield_generator" then local num = db:get_int("checkpoint:pre_vix_fields_broken") +1 db:set_int("checkpoint:pre_vix_fields_broken", num) if num == 1 then -- Key breaks his first generator. minetest.after(1, function() artifact.push_chat_message("Hehe.", "Key", "artifact_key_splash_low.png") end) elseif num == 5 then -- All generators are down. vix_scene._can_check = nil end end end end end function ns.enter_main_state() vix_scene.object:remove() end function ns.enter_state(to) state = to if state == ns.states.init then ns.enter_init_state() elseif state == ns.states.pre_vix then ns.enter_pre_vix_state() elseif state == ns.states.main then ns.enter_main_state() end db:set_int("state", state) end function ns.get_state() return state end -- Used for marking the start position in schematics. -- Disappears when not in debug mode. artifact.register_node("start_pos", { drawtype = not artifact.debug and "airlike" or nil, paramtype = "light", walkable = artifact.debug or false, pointable = artifact.debug or false, tiles = {artifact.debug and "artifact_start_pos.png" or "blank.png"} }) function artifact.look_at(m, pos, pos2) local rot = (pos2 and pos or m.object:get_pos()):direction(pos2 or pos):dir_to_rotation() m.object:set_look_horizontal(rot.y) -- Pitch seems to be flipped on the player? m.object:set_look_vertical(-rot.x) end minetest.register_chatcommand("splash", { func = function(name) local m = artifact.players[name] minetest.show_formspec(m.name, "artifact:lock_camera", [[ formspec_version[10] size[32,18] padding[0,0] bgcolor[#000] animated_image[0,0;32,18;;artifact_splash.png;60;100;;] ]]) end }) -- Play the opening cutscene. function ns.play_intro_cutscene() ns.camera = minetest.add_entity(artifact.origin:offset(0,-0.75,0), "display") ns.scene = minetest.add_entity(artifact.origin:offset(-2.25,-0.5,7 -1/16), "display") ns.scene:set_properties { visual = "mesh", mesh = "artifact_cutscene_a.gltf", textures = {"artifact_key.png", "artifact_statue.png"} } ns.scene:set_animation({x=0,y=25}, 1, 0.1, false) for _, m in pairs(artifact.players) do m.object:set_attach(ns.camera) minetest.show_formspec(m.name, "artifact:lock_camera", [[ formspec_version[10] size[32,18] padding[0,0] allow_close[false] bgcolor[#0000] ]]) m.object:set_look_vertical(0) m.object:set_look_horizontal(0) end -- Begin mess. minetest.after(17, function() for x = -1, 1 do for z = -1, 1 do minetest.remove_node(artifact.origin:offset(x, -1, z -5)) minetest.add_particlespawner { pos = { min = artifact.origin:offset(x -0.5, -1, z -5 -0.5), max = artifact.origin:offset(x +0.5, -0.5, z -5 +0.5) }, vel = { min = vector.new(-1, 0, -1) *1.5, max = vector.new(1, 2, 1) *1.5 }, acc = vector.new(0,-9.81,0), collisiondetection = true, amount = 50, node = {name = "artifact:stone_tile_brown"}, time = 0.1 } end end end) minetest.after(3.5, function() -- Slowly move back as Key walks forward. ns.camera:set_acceleration(vector.new(0,0,-0.5)) minetest.after(1, function() -- Decelerate before switching angles, for smoothness. ns.camera:set_acceleration(vector.new(0,0,0.3)) end) minetest.after(2, function() ns.camera:set_acceleration(vector.zero()) ns.camera:set_velocity(vector.zero()) ns.camera:set_pos(artifact.origin:offset(-5, 3, -4)) for _, m in pairs(artifact.players) do artifact.look_at(m, ns.camera:get_pos(), artifact.origin:offset(0,0,1)) end local time = minetest.get_us_time() -- Pan to follow Key as he moves toward the pedestal. local function interpolate() local fac = (minetest.get_us_time() -time) /4000000 local offset = artifact.interpolate(1, 4, fac) for _, m in pairs(artifact.players) do artifact.look_at(m, ns.camera:get_pos(), artifact.origin:offset(0,0,-offset)) end -- Do a globalstep callback the lazy way. if fac < 1 then minetest.after(0, interpolate) end end minetest.after(0, interpolate) minetest.after(4, function() -- Dramatically move backward as Key stares at the statue. ns.camera:set_pos(artifact.origin:offset(-0.2, -0.5, -9)) ns.camera:set_velocity(vector.new(0,0,-0.5)) for _, m in pairs(artifact.players) do m.object:set_look_vertical(0) m.object:set_look_horizontal(0) end minetest.after(6, function() -- Cut back to where we were before, so we get a good view of Key falling in the hole. ns.camera:set_pos(artifact.origin:offset(-5, 3, -4)) ns.camera:set_velocity(vector.new(0,0,0)) for _, m in pairs(artifact.players) do artifact.look_at(m, ns.camera:get_pos(), artifact.origin:offset(0,0,-4)) end minetest.after(3, function() -- Show epic splash animation while Key finishes falling down the hole. for _, m in pairs(artifact.players) do artifact.hud_add(m, { type = "image", name = "background", pos = {x=0.5,y=0.5}, scale = {x=1000,y=1000}, image = "[fill:16x16:0,0:#000", opacity = 0 }) m.hud.background:animate { opacity = { value = 256, duration = 0.3 } } end minetest.after(0.3, function() for _, m in pairs(artifact.players) do minetest.show_formspec(m.name, "artifact:lock_camera", [[ formspec_version[10] size[32,18] padding[0,0] allow_close[false] bgcolor[#0000] animated_image[0,0;32,18;;artifact_splash.png;60;100;;] ]]) end end) minetest.after(6.3, function() for _, m in pairs(artifact.players) do minetest.show_formspec(m.name, "artifact:lock_camera", [[ formspec_version[10] size[32,18] padding[0,0] allow_close[false] bgcolor[#0000] ]]) m.hud.background:animate { opacity = { value = 0, duration = 0.3 } } m.hud.background.remove_after = 0.3 end ns.camera:set_pos(artifact.origin:offset(-1, -73, -6)) ns.camera:set_velocity(vector.new(0,0,0)) for _, m in pairs(artifact.players) do artifact.look_at(m, ns.camera:get_pos(), artifact.origin:offset(1, -74, -3)) end ns.scene:remove() ns.scene = minetest.add_entity(artifact.origin:offset(-1, -73.5, -6), "display") ns.scene:set_properties { visual = "mesh", mesh = "artifact_cutscene_b.gltf", textures = {"artifact_key.png", "artifact_blackrod.png"} } minetest.after(0.3, function() artifact.push_chat_message("Ow.", "Key", "artifact_key_splash_low.png") minetest.after(1, function() ns.scene:set_animation({x=0,y=25}, 1, 0.1, false) end) minetest.after(9, function() artifact.push_chat_message("Interesting...", "Key", "artifact_key_splash_low.png") end) minetest.after(13, function() ns.scene:remove() for _, m in pairs(artifact.players) do m.object:set_detach() minetest.close_formspec(m.name, "artifact:lock_camera") artifact.look_at(m, ns.camera:get_pos(), artifact.origin:offset(0, -73.5, -8)) end ns.enter_state(ns.states.pre_vix) minetest.after(3, function() artifact.push_chat_message("I'd better take a few practice swings.", "Key", "artifact_key_splash_low.png") end) end) end) end) end) end) end) end) end) -- End mess. end -- Do mapgen. This isn't technically story-related, but it's here -- anyway because we only need to do it for state == "loading" -- and it's the only mapgen we do. function ns.load_map() -- Notify the player that we must mapgen first. for _, m in pairs(artifact.players) do artifact.hud_add(m, { type = "image", name = "loading_map_bg", pos = {x=0.5,y=0.5}, scale = {x=1000,y=1000}, -- Cover the whole window without having to get the window size. image = "[fill:16x16:0,0:#000" }) artifact.hud_add(m, { type = "text", name = "loading_map", text = "Loading map...", pos = {x=0.5,y=0.5}, size = {x=3,y=0}, color = minetest.colorspec_to_table("#000") }) m.hud.loading_map:animate { color = { value = minetest.colorspec_to_table("#888"), duration = 0.3, } } end -- Make sure the loading HUD fades in first. minetest.after(0.3, function() -- Emerge the area so we have something to write to. -- This is one of the relatively few cases where the -- Promise API is actually more than a little helpful, -- because we can simply 'yield' until the emerge is -- completely finished in a semi-clean way. Promise(function(r) minetest.emerge_area(vector.new(-1,-1,-1) *100, vector.new(1, 1, 1) *100, function(bpos, action, remaining) if remaining == 0 then r() end end) end).after(function() for _, m in pairs(artifact.players) do m.hud.loading_map:animate { color = { value = minetest.colorspec_to_table("#000"), duration = 0.3, } } end -- The mapgen code is here, but the actual world schematic should live in artifact_world. local path = minetest.get_modpath("artifact_world").."/schems/map" local pos1 = vector.new(-5,-7,-5) local pos2 = pos1 +artifact.get_schem_size(path) artifact.load_schematic(pos1, path) -- Wait a bit to make quite sure that the schematic is in place (and allow the HUD to fade out). minetest.after(0.3, function() local nodes = minetest.find_nodes_in_area(pos1, pos2, "start_pos") -- If we can't find a start marker, fall back to the global origin. local start = nodes[1] or vector.zero() artifact.origin = start db:set_string("origin", start:to_string()) for _, m in pairs(artifact.players) do m.hud.loading_map:remove(m) m.hud.loading_map_bg:animate { opacity = { value = 0, duration = 0.3 } } m.hud.loading_map_bg.remove_after = 0.3 m.object:set_pos(start) end ns.enter_state(artifact.story.states.pre_vix) end) end) end) end include "objectives.lua" local started = false minetest.register_on_joinplayer(function(p) local m = artifact.players[p:get_player_name()] -- Only add the HUD etc. if the player is actually in the game. if state == ns.states.init then m.object:set_attach(ns.camera) minetest.show_formspec(m.name, "artifact:lock_camera", [[ formspec_version[10] allow_close[false] bgcolor[#0000] ]]) elseif state > ns.states.init then m:add_health_bar() m.object:hud_set_flags { crosshair = true, wielditem = true } end -- So we only call this when the _first_ player joins. -- Sure, we're technically a singleplayer game, but, -- as they say, better to have it and not need it than -- need it and not have it. if not started then started = true if state == ns.states.loading then ns.load_map() elseif state == "init" then ns.play_intro_cutscene() end end end) if artifact.debug then minetest.register_chatcommand("map", { func = function() local path = minetest.get_modpath("artifact_world").."/schems/map" local pos1 = vector.new(-5,-7,-5) artifact.load_schematic(pos1, path) end }) end