Add the intro cutscene, a start to the map, and various other things.

This commit is contained in:
Signal 2025-11-16 02:48:08 -05:00
parent d0c0a3ebb6
commit 1b2199705b
46 changed files with 1401 additions and 91 deletions

View file

@ -1,19 +1,49 @@
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", -- For mapgen
"init", -- For the opening cutscene
"pre_vix", -- The player doesn't have Vix yet
"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)")
function ns.set_state(to)
if artifact.debug then state = ns.states.main end
function ns.enter_init_state()
ns.play_intro_cutscene()
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,
}
end
end
function ns.enter_state(to)
state = to
minetest.log("State changed to "..to..".")
if state == ns.states.init then
ns.enter_init_state()
elseif state == ns.states.pre_vix then
ns.enter_pre_vix_state()
end
db:set_int("state", state)
end
@ -21,12 +51,330 @@ function ns.get_state()
return state
end
function ns.before_state(target)
-- 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_globalstep(function()
if state == "init" 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
})
-- 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")
m.object:set_pos(artifact.origin:offset(0, -73.5, -4))
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("Interesting...", "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()]
-- 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

View file

@ -1,2 +1,2 @@
name = artifact_story
depends = artifact_base
depends = artifact_player

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,15 @@
local ns = artifact
ns.objectives = {}
function ns.establish_objective()
end
function ns.complete_objective()
end
function ns.update_objective()
end

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 B