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.
        "ended", -- 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"},
        backface_culling = false
    },
    on_activate = function(e)
        if state > ns.states.pre_vix then
            e.object:remove()
            return
        end
        e.object:set_armor_groups{immortal = 1}
        e.object:set_animation({x=0,y=2}, 0.5, 0.1, true)
        
        e.particles = minetest.add_particlespawner {
            pos = e.object:get_pos():offset(0, 0.15, 0),
            vel = {
                min = vector.new(-0.5, 0,-0.5),
                max = vector.new( 0.5, 0, 0.5)
            },
            acc = vector.new(0, -9.81, 0),
            texture = {
                name = "artifact_light_gold.png",
                alpha_tween = {0.75, 0}
            },
            collisiondetection = true,
            time = 0,
            amount = 100,
            exptime = 5
        }
        e._track_pos = e.object:get_pos():offset(0, 0.5, 0)
        e._rot = vector.zero()
        
        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
        elseif e._track then
            local m = artifact.players[next(artifact.players)]
            local rot = e._track_pos:direction(m.pos):rotate(e._rot):dir_to_rotation()
            if rot.y > -math.pi *(2/3) and rot.y < math.pi *(2/3) then
                rot.y = math.pi *(2/3) *math.sign(rot.y)
            end
            rot.y = -rot.y +math.pi
            rot.x = -math.max(-math.pi /3, math.min(math.pi /3, rot.x)) +0.25
            
            e.object:set_bone_override("Head", {
                rotation = {
                    vec = rot,
                    interpolation = 0.4
                }
            })
        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 type == "node" and 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
                    -- Wait just a bit, so the player can look up.
                    minetest.after(0.5, function()
                        ns.play_vix_scene()
                    end)
                end
            end
        end
    end
end

function ns.enter_main_state()
    minetest.add_entity(vix_scene.object:get_pos():offset(1.5, -0.8, -0.5), "artifact:sidekick"):set_rotation(vector.new(0, -math.pi /2, 0))
    vix_scene.object:remove()
end

function ns.enter_ended_state()
    for _, m in pairs(artifact.players) do
        local bg = artifact.hud_add(m, {
            type = "image",
            image = "[fill:16x16:0,0:#000",
            opacity = 0,
            pos = {x=0.5,y=0.5},
            scale = {x=1000,y=1000},
        })
        bg:animate {
            opacity = {
                value = 256,
                duration = 0.3
            }
        }
        minetest.after(0.3, function()
            local txt = artifact.hud_add(m, {
                type = "text",
                color = "#000",
                text = "To be continued...",
                pos = {x=0.5,y=0.5},
                size = {x=3,y=0},
                color = minetest.colorspec_to_table("#000")
            })
            txt:animate {
                color = {
                    value = minetest.colorspec_to_table("#888"),
                    duration = 0.3
                }
            }
            minetest.after(8, function()
                txt:animate {
                        color = {
                        value = minetest.colorspec_to_table("#000"),
                        duration = 0.3
                    }
                }
                minetest.after(0.3, function()
                    minetest.request_shutdown("You completed the game.")
                end)
            end)
        end)
    end
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()
    elseif state == ns.states.ended then
        ns.enter_ended_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

if artifact.debug then
    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
    })
    
    minetest.register_chatcommand("flash", {
        func = function(name)
            minetest.add_particle {
                pos = artifact.origin:offset(-16.5, -71.5, -17),
                velocity = vector.zero(),
                texture = {
                    name = "artifact_flash.png",
                    alpha_tween = {0, 1}
                },
                size = 60,
                expirationtime = 0.1
            }
        end
    })
end

artifact.register_node("stasis_beacon", {
    drawtype = "mesh",
    mesh = "artifact_stasis_beacon.gltf",
    tiles = {"artifact_stasis_beacon.png"},
    use_texture_alpha = "blend",
    paramtype = "light"
})

-- The 'cutscene' playerd after the player frees Vix.
function ns.play_vix_scene()
    -- Kaboom.
    minetest.add_particle {
        pos = artifact.origin:offset(-16.5, -71.5, -17),
        velocity = vector.zero(),
        texture = {
            name = "artifact_flash.png",
            alpha_tween = {0, 1}
        },
        size = 60,
        expirationtime = 0.1
    }
    artifact.play_sound {
        name = "artifact_free_vix",
        pos = artifact.origin:offset(-16.5, -72.5, -17)
    }
    vix_scene.object:set_animation({x=3,y=150}, 1, 0.1, false)
    minetest.delete_particlespawner(vix_scene.particles)
    vix_scene.particles = nil
    -- Key can walk around freely, so Vix should try to face him once she wakes.
    local i = 7
    minetest.after(i, function()
        vix_scene._track = true
    end)
    i = i +1
    minetest.after(i, function()
        -- Is he the one they sent?
        artifact.push_chat_message("...Who are you?", minetest.colorize("#f5dd66", "???"), "artifact_vix_splash_low.png")
    end)
    i = i +4
    minetest.after(i, function()
        -- Awkward.
        artifact.push_chat_message("Uh... Wow, this is awkward. I did not think that through far enough.", "Key", "artifact_key_splash_low.png")
    end)
    i = i +7
    minetest.after(i, function()
        -- Oops, that didn't help.
        artifact.push_chat_message("Wait, that was bad, let's try this again.", "Key", "artifact_key_splash_low.png")
    end)
    i = i +4
    minetest.after(i, function()
        -- Better.
        artifact.push_chat_message("Hi. I'm Key.", "Key", "artifact_key_splash_low.png")
    end)
    i = i +4
    minetest.after(i, function()
        -- ...He's not?
        artifact.push_chat_message("...Hi.", minetest.colorize("#f5dd66", "???"), "artifact_vix_splash_low.png")
        -- [ She sits up ]
        vix_scene._track = nil
        vix_scene.object:set_bone_override("Head", {
            rotation = {
                vec = vector.zero(), interpolation = 0.4
            }
        })
    end)
    i = i +5
    minetest.after(i, function()
        vix_scene._track = true
        -- So what's the deal?
        artifact.push_chat_message("How did you get here?", minetest.colorize("#f5dd66", "???"), "artifact_vix_splash_low.png")
    end)
    i = i +5
    minetest.after(i, function()
        artifact.push_chat_message("It's actually a long story, but the short version is that I fell in a hole.", "Key", "artifact_key_splash_low.png")
    end)
    i = i +4
    minetest.after(i, function()
        artifact.push_chat_message("...", minetest.colorize("#f5dd66", "???"), "artifact_vix_splash_low.png")
    end)
    i = i +4
    minetest.after(i, function()
        artifact.push_chat_message("Maintenance might want to have a look at the closet over there.", "Key", "artifact_key_splash_low.png")
    end)
    i = i +6
    minetest.after(i, function()
        -- Okay... Wait, how long has it been, anyway? The room is awfully overgrown...
        artifact.push_chat_message("I don't think maintenance will mind...", minetest.colorize("#f5dd66", "???"), "artifact_vix_splash_low.png")
    end)
    i = i +4
    minetest.after(i, function()
        artifact.push_chat_message("Probably so...", "Key", "artifact_key_splash_low.png")
        -- [ Vix climbs off the pedestal ]
        vix_scene._track = nil
        vix_scene.object:set_bone_override("Head", {
            rotation = {
                vec = vector.zero(), interpolation = 0.4
            }
        })
        vix_scene._track_pos = vix_scene.object:get_pos():offset(1.5, 0, -0.5)
        vix_scene._rot = vector.new(0, -math.pi /2, 0)
    end)
    i = i +7
    minetest.after(i, function()
        vix_scene._track = true
        artifact.push_chat_message("So, uh, what's _your_ name?", "Key", "artifact_key_splash_low.png")
    end)
    i = i +4
    minetest.after(i, function()
        artifact.push_chat_message("...Vix.", "Vix", "artifact_vix_splash_low.png")
    end)
    i = i +4
    minetest.after(i, function()
        artifact.push_chat_message("Formally pleased to meet you.", "Key", "artifact_key_splash_low.png")
    end)
    i = i +5
    minetest.after(i, function()
        -- How trustworthy is he?
        artifact.push_chat_message("Why did you... break the forcefields?", "Vix", "artifact_vix_splash_low.png")
    end)
    i = i +4
    minetest.after(i, function()
        artifact.push_chat_message("Well, when I saw you locked inside all those forcefields, I thought 'Wow, she doesn't look too happy'.", "Key", "artifact_key_splash_low.png")
    end)
    i = i +7
    minetest.after(i, function()
        artifact.push_chat_message("Then I thought, 'Hmm, she must have been there a while, what with all the vines over here and not over there.'", "Key", "artifact_key_splash_low.png")
    end)
    i = i +6
    minetest.after(i, function()
        artifact.push_chat_message("_Then_ I thought, 'Whoever's fault this is probably isn't going to be back. Why not help her out?'", "Key", "artifact_key_splash_low.png")
    end)
    i = i +7
    minetest.after(i, function()
        artifact.push_chat_message("So then I used this cool stick I found to trash all the forcefields and let you out.", "Key", "artifact_key_splash_low.png")
    end)
    i = i +7
    minetest.after(i, function()
        -- ..._Are_ they going to be back...?
        artifact.push_chat_message("What year is it?", "Vix", "artifact_vix_splash_low.png")
    end)
    i = i +4
    minetest.after(i, function()
        artifact.push_chat_message("I dunno, I lost count a while back. Somewhere around the turn of the century.", "Key", "artifact_key_splash_low.png")
    end)
    i = i +5
    minetest.after(i, function()
        -- ... I guess not.
        artifact.push_chat_message("...", "Vix", "artifact_vix_splash_low.png")
    end)
    i = i +4
    minetest.after(i, function()
        -- You know what, what am I even doing here?
        -- (It's not like staying is a workable option, but that's not really what Vix has in mind.)
        artifact.push_chat_message("We need to leave... I don't suppose we could get out the way you got in?", "Vix", "artifact_vix_splash_low.png")
    end)
    i = i +6
    minetest.after(i, function()
        artifact.push_chat_message("...Nah.", "Key", "artifact_key_splash_low.png")
    end)
    i = i +4
    minetest.after(i, function()
        artifact.push_chat_message("I guess that leaves the hard way.", "Vix", "artifact_vix_splash_low.png")
    end)
    i = i +4
    minetest.after(i, function()
        artifact.push_chat_message("Let's see if this works...", "Vix", "artifact_vix_splash_low.png")
        minetest.after(1, function()
            local burst = minetest.add_entity(vix_scene.object:get_pos():offset(0, 1, -1.5), "artifact:burst")
            burst:get_luaentity()._critical = true
            burst:set_attach(vix_scene.object, "RightArm", vector.new(0, 100, 0))
            minetest.after(0, function()
                burst:set_detach()
                artifact.play_sound {
                    name = "artifact_burst_fire",
                    pos = burst:get_pos()
                }
                burst:get_luaentity():impulse(burst:get_pos():direction(artifact.origin:offset(18, -71, -13)) *30)
            end)
        end)
    end)
    i = i +8
    minetest.after(i, function()
        artifact.push_chat_message("Wow, that's epic.", "Key", "artifact_key_splash_low.png")
    end)
    i = i +2
    minetest.after(i, function()
        ns.enter_state(ns.states.main)
        minetest.after(10, function()
            local m = artifact.players[next(artifact.players)]
            if m.character ~= "vix" then
                artifact.show_help_message(m, "You can switch between Key and Vix using the Drop control.", "info")
            end
        end)
    end)
    i = i +45
    minetest.after(i, ns.dialogue_1)
end

function ns.dialogue_1()
    local i = 0
    artifact.push_chat_message("So, what's the long version of you you got here?", "Vix", "artifact_vix_splash_low.png")
    i = i +5
    minetest.after(i, function()
        artifact.push_chat_message("Well, one fine day, I was strolling along through a little town called Birchwood.", "Key", "artifact_key_splash_low.png")
    end)
    i = i +5
    minetest.after(i, function()
        artifact.push_chat_message("On the street corner, some guy was doing a huge ad for an adventuring supply company.", "Key", "artifact_key_splash_low.png")
    end)
    i = i +6
    minetest.after(i, function()
        artifact.push_chat_message("Naturally, I suggested that his wares were overpriced because he was spending so much on advertising.", "Key", "artifact_key_splash_low.png")
    end)
    i = i +6
    minetest.after(i, function()
        artifact.push_chat_message("I won't bore you with the details, but there was a whole thing that ended with me betting him five gold pieces that I could steal some artifact whose name I forget without any supplies at all.", "Key", "artifact_key_splash_low.png")
    end)
    i = i +8
    minetest.after(i, function()
        artifact.push_chat_message("(I told him it was probably guarded by a few bats and a couple average-sized spiders, but he wouldn't believe me.)", "Key", "artifact_key_splash_low.png")
    end)
    i = i +6
    minetest.after(i, function()
        artifact.push_chat_message("I was wrong about there not being any traps, though. That's why I'm down here and not five gold pieces richer. (Yet.)", "Key", "artifact_key_splash_low.png")
    end)
    i = i +6
    minetest.after(i, function()
        artifact.push_chat_message("So you came all this way for five gold pieces?", "Vix", "artifact_vix_splash_low.png")
    end)
    i = i +5
    minetest.after(i, function()
        artifact.push_chat_message("No, no, I came all this way to prove a point. I'd probably forget I had the gold pieces within the hour.", "Key", "artifact_key_splash_low.png")
    end)
    i = i +6
    minetest.after(i, function()
        artifact.push_chat_message("Wow.", "Vix", "artifact_vix_splash_low.png")
    end)
    i = i +15
    minetest.after(i, function()
        artifact.push_chat_message("So, how about you? What's the story with that awesome burst ability?", "Key", "artifact_key_splash_low.png")
    end)
    i = i +5
    minetest.after(i, function()
        artifact.push_chat_message("Have you ever heard of Iron Dragon?", "Vix", "artifact_vix_splash_low.png")
    end)
    i = i +4
    minetest.after(i, function()
        artifact.push_chat_message("Nope.", "Key", "artifact_key_splash_low.png")
    end)
    i = i +5
    minetest.after(i, function()
        artifact.push_chat_message("They're... Well, they were... a vigilante group, who were fairly powerful fifty-some years ago. My father was one of the generals.", "Vix", "artifact_vix_splash_low.png")
    end)
    i = i +6
    minetest.after(i, function()
        artifact.push_chat_message("Early on, they learned to synthesize a controlled form of energy to power tools; you saw some of that in the forcefields.", "Vix", "artifact_vix_splash_low.png")
    end)
    i = i +6
    minetest.after(i, function()
        artifact.push_chat_message("Eventually, someone — my aunt, actually — floated the idea that feeding that energy into a human might give them new abilities.", "Vix", "artifact_vix_splash_low.png")
    end)
    i = i +6
    minetest.after(i, function()
        artifact.push_chat_message("No one liked it at first. Eventually, though, when we were weakened and all but defeated, my father became desperate enough to consider it.", "Vix", "artifact_vix_splash_low.png")
    end)
    i = i +6
    minetest.after(i, function()
        artifact.push_chat_message("He wanted me to be the first test subject. Looking back, I think he wanted to protect me, keep me out of the combat if things got that bad.", "Vix", "artifact_vix_splash_low.png")
    end)
    i = i +6
    minetest.after(i, function()
        artifact.push_chat_message("I trusted his judgement.", "Vix", "artifact_vix_splash_low.png")
    end)
    i = i +4
    minetest.after(i, function()
        artifact.push_chat_message("Incredibly, though, he was right... It makes no sense physiologically, but somehow it worked.", "Vix", "artifact_vix_splash_low.png")
    end)
    i = i +8
    minetest.after(i, function()
        artifact.push_chat_message("Now here I am half a century later, and it's all gone...", "Vix", "artifact_vix_splash_low.png")
    end)
    i = i +6
    minetest.after(i, function()
        artifact.push_chat_message("Wow. Imagine an experiment that worked on the first try.", "Key", "artifact_key_splash_low.png")
    end)
    i = i +4
    minetest.after(i, function()
        artifact.push_chat_message("Right? I though there could be something to it, but this? It's been half a century, and I feel even better than I did before.", "Vix", "artifact_vix_splash_low.png")
    end)
    i = i +6
    minetest.after(i, function()
        artifact.push_chat_message("That's what I was thinking. You look sixteen, not 60.", "Key", "artifact_key_splash_low.png")
    end)
    i = i +4
    minetest.after(i, function()
        artifact.push_chat_message("Really?", "Vix", "artifact_vix_splash_low.png")
    end)
    i = i +4
    minetest.after(i, function()
        artifact.push_chat_message("Yep.", "Key", "artifact_key_splash_low.png")
    end)
    i = i +4
    minetest.after(i, function()
        artifact.push_chat_message("...Huh. That's...", "Vix", "artifact_vix_splash_low.png")
    end)
    i = i +4
    minetest.after(i, function()
        artifact.push_chat_message("Really weird?", "Key", "artifact_key_splash_low.png")
    end)
    i = i +4
    minetest.after(i, function()
        artifact.push_chat_message("Yeah. That's exactly how old I was when this happened...", "Vix", "artifact_vix_splash_low.png")
    end)
    i = i +5
    minetest.after(i, function()
        artifact.push_chat_message("Most intriguing...", "Key", "artifact_key_splash_low.png")
    end)
end

-- Play the final scene.
function ns.play_final_scene()
    local scn = minetest.add_entity(artifact.origin:offset(132, -69.5, -22), "display")
    scn:set_properties {
        visual = "mesh",
        mesh = "artifact_scene_final.gltf",
        textures = {"artifact_tav.png"}
    }
    local i = 2
    minetest.after(i, function()
        artifact.push_chat_message("Hey, who's that?", "Key", "artifact_key_splash_low.png")
        minetest.after(1, function()
            scn:set_animation({x=0,y=120}, 1, 0, false)
        end)
    end)
    i = i +4
    minetest.after(i, function()
        artifact.push_chat_message("I don't know...", "Vix", "artifact_vix_splash_low.png")
    end)
    i = i +4
    minetest.after(i, function()
        artifact.push_chat_message("You.", minetest.colorize("#666", "???"), "artifact_tav_splash_low.png")
    end)
    i = i +3
    minetest.after(i, function()
        artifact.push_chat_message("Huh?", "Key", "artifact_key_splash_low.png")
    end)
    i = i +5
    minetest.after(i, function()
        artifact.push_chat_message("What have you done?", minetest.colorize("#666", "???"), "artifact_tav_splash_low.png")
    end)
    i = i +4
    minetest.after(i, function()
        artifact.push_chat_message("I just walked in here, why?", "Key", "artifact_key_splash_low.png")
    end)
    i = i +5
    minetest.after(i, function()
        artifact.push_chat_message("Vix.", minetest.colorize("#666", "???"), "artifact_tav_splash_low.png")
    end)
    i = i +4
    minetest.after(i, function()
        artifact.push_chat_message("What?", "Vix", "artifact_vix_splash_low.png")
    end)
    i = i +5
    minetest.after(i, function()
        artifact.push_chat_message("Hm.", minetest.colorize("#666", "???"), "artifact_tav_splash_low.png")
    end)
    i = i +3
    minetest.after(i, function()
        artifact.push_chat_message("So you really did?", minetest.colorize("#666", "???"), "artifact_tav_splash_low.png")
    end)
    i = i +4
    minetest.after(i, function()
        artifact.push_chat_message("I don't understand...", "Vix", "artifact_vix_splash_low.png")
    end)
    i = i +5
    minetest.after(i, function()
        artifact.push_chat_message("Is that so?", minetest.colorize("#666", "???"), "artifact_tav_splash_low.png")
    end)
    i = i +4
    minetest.after(i, function()
        artifact.push_chat_message("Yes?", "Vix", "artifact_vix_splash_low.png")
    end)
    i = i +4
    minetest.after(i, function()
        artifact.push_chat_message("Traitor.", minetest.colorize("#666", "???"), "artifact_tav_splash_low.png")
    end)
    i = i +2
    minetest.after(i, function()
        artifact.push_chat_message("Wow.", "Key", "artifact_key_splash_low.png")
    end)
    i = i +2
    minetest.after(i, function()
        artifact.push_chat_message("Huh? Iron Dragon is gone. What are you accusing me of?", "Vix", "artifact_vix_splash_low.png")
    end)
    i = i +4
    minetest.after(i, function()
        artifact.push_chat_message("So you don't know. Unfortunate.", minetest.colorize("#666", "???"), "artifact_tav_splash_low.png")
    end)
    i = i +4
    minetest.after(i, function()
        artifact.push_chat_message("Who are you, anyway?", "Key", "artifact_key_splash_low.png")
    end)
    i = i +4
    minetest.after(i, function()
        artifact.push_chat_message("A ghost.", minetest.colorize("#666", "???"), "artifact_tav_splash_low.png")
    end)
    i = i +4
    minetest.after(i, function()
        artifact.push_chat_message("Whoa, stop being so ambiguous! I don't think we know what you think we know.", "Key", "artifact_key_splash_low.png")
    end)
    i = i +5
    minetest.after(i, function()
        artifact.push_chat_message("I'd love to banter, but right now I have more important things to do.", minetest.colorize("#666", "???"), "artifact_tav_splash_low.png")
    end)
    i = i +5
    minetest.after(i, function()
        artifact.push_chat_message("As far as I'm concerned, you can rot down here — mother.", minetest.colorize("#666", "???"), "artifact_tav_splash_low.png")
        minetest.after(1, function()
            local door = artifact.large_doors[artifact.origin:offset(137, -69, -22):round():to_string()]
            door:open()
            minetest.after(3, function()
                door:close()
            end)
        end)
    end)
    i = i +5
    minetest.after(i, function()
        artifact.push_chat_message("Excuse me!?", "Key", "artifact_key_splash_low.png")
        artifact.push_chat_message("Excuse me!?", "Vix", "artifact_vix_splash_low.png")
    end)
    i = i +3
    minetest.after(i, function()
        artifact.push_chat_message("Hey! You're being so cliche right now!", "Key", "artifact_key_splash_low.png")
    end)
    i = i +7
    minetest.after(i, function()
        artifact.push_chat_message("Well, that was weird.", "Key", "artifact_key_splash_low.png")
    end)
    i = i +9
    minetest.after(i, function()
        artifact.push_chat_message("...So, Vix, you wouldn't happen to know of any ventilation shafts conveniently placed nearby, would you?", "Key", "artifact_key_splash_low.png")
    end)
    i = i +6
    minetest.after(i, function()
        artifact.push_chat_message("Well, there is one...", "Vix", "artifact_vix_splash_low.png")
    end)
    i = i +4
    minetest.after(i, function()
        artifact.push_chat_message("Nice. Let's get out of this place.", "Key", "artifact_key_splash_low.png")
    end)
    i = i +4
    minetest.after(i, function()
        ns.enter_state(ns.states.ended)
    end)
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.init)
            end)
        end)
    end)
end

include "objectives.lua"

local started = false
minetest.register_on_joinplayer(function(p)
    local m = artifact.players[p:get_player_name()]
    
    if state == ns.states.ended then
        minetest.request_shutdown("You completed the game.")
    end
    
    -- 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
    
    if state == ns.states.pre_vix and db:get_int("checkpoint:pre_vix_fields_broken") >= 5 then
        -- If the player left during the scene for some reason, start it over when they join back.
        ns.play_vix_scene()
    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

